[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: jimbethancourt\n#open_collective: RefactorFirst\n#ko_fi: jimbethancourt\n#liberapay: jimbethancourt\n#patreon: jimbethancourt\n"
  },
  {
    "path": ".github/workflows/codesee-arch-diagram.yml",
    "content": "# This workflow was added by CodeSee. Learn more at https://codesee.io/\n# This is v2.0 of this workflow file\non:\n  push:\n    branches:\n      - main\n  pull_request_target:\n    types: [opened, synchronize, reopened]\n\nname: CodeSee\n\npermissions: read-all\n\njobs:\n  codesee:\n    runs-on: ubuntu-latest\n    continue-on-error: true\n    name: Analyze the repo with CodeSee\n    steps:\n      - uses: Codesee-io/codesee-action@v2\n        with:\n          codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/maven-pr.yml",
    "content": "# This workflow will build a Java project with Maven\n# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven\n\nname: Java CI with Maven (PR)\n\non:\n  pull_request:\n    branches: [ main ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@v4\n\n      - name: Set up JDK 11\n        uses: actions/setup-java@v4\n        with:\n          java-version: 11\n          distribution: 'zulu'\n\n      - name: Build With Maven\n        run: mvn -B verify\n\n\n#      Comment \"Build With Maven\" and uncomment the below when you want a snapshot build to be deployed\n#      *********Don't forget to switch to Java 1.8 as well********\n#      - name: Publish Maven snapshot\n#        uses: samuelmeuli/action-maven-publish@v1\n#        with:\n#          gpg_private_key: ${{ secrets.gpg_private_key }}\n#          gpg_passphrase: ${{ secrets.gpg_passphrase }}\n#          nexus_username: ${{ secrets.nexus_username }}\n#          nexus_password: ${{ secrets.nexus_password }}\n#          maven_profiles: snapshot-release\n"
  },
  {
    "path": ".github/workflows/maven.yml",
    "content": "# This workflow will build a Java project with Maven\n# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven\n\nname: Java CI with Maven\n\non:\n  push:\n    branches: [ main ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@v4\n\n      - name: Set up JDK 11\n        uses: actions/setup-java@v4\n        with:\n          java-version: 11\n          distribution: 'zulu'\n\n      - name: Build With Maven\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}  # Needed to get PR information, if any\n        run: mvn -B verify\n\n\n#      Comment \"Build With Maven\" and uncomment the below when you want a snapshot build to be deployed\n#      *********Don't forget to switch to Java 1.8 as well********\n#      - name: Publish Maven snapshot\n#        uses: samuelmeuli/action-maven-publish@v1\n#        with:\n#          gpg_private_key: ${{ secrets.gpg_private_key }}\n#          gpg_passphrase: ${{ secrets.gpg_passphrase }}\n#          nexus_username: ${{ secrets.nexus_username }}\n#          nexus_password: ${{ secrets.nexus_password }}\n#          maven_profiles: snapshot-release\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "# Based on https://github.com/jagodevreede/semver-check/blob/c9353fa86eb9ae8f6b309057748672a6c1e0f435/.github/workflows/release.yml\n# From https://foojay.io/today/how-to-publish-a-java-maven-project-to-maven-central-using-jreleaser-and-github-actions-2025-guide/\n# If this doesn't work, try https://jreleaser.org/guide/latest/continuous-integration/github-actions.html\nname: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Release version'\n        required: true\n      nextVersion:\n        description: 'Next version after release (-SNAPSHOT will be added automatically)'\n        required: true\n\njobs:\n  build:\n    permissions:\n      contents: write\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      - name: Set up JDK 11\n        uses: actions/setup-java@v3\n        with:\n          java-version: '11'\n          distribution: 'temurin'\n          cache: maven\n      - name: Set release version\n        run: mvn --no-transfer-progress --batch-mode versions:set -DnewVersion=${{ github.event.inputs.version }}\n\n      - name: Commit & Push changes\n        uses: actions-js/push@master\n        with:\n          github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }}\n          message: \"build: Releasing version ${{ github.event.inputs.version }}\"\n\n      - name: Stage release\n        run: mvn --no-transfer-progress --batch-mode -Ppublish clean deploy -DaltDeploymentRepository=local::default::file://`pwd`/target/staging-deploy\n\n      - name: Run JReleaser\n        uses: jreleaser/release-action@v2\n        with:\n          setup-java: false\n          version: 1.20.0\n        env:\n          JRELEASER_PROJECT_VERSION: ${{ github.event.inputs.version }}\n          JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }}\n          JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}\n          JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}\n          JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}\n          JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_RELEASE_DEPLOY_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME }}\n          JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_RELEASE_DEPLOY_PASSWORD: ${{ secrets.JRELEASER_MAVENCENTRAL_SONATYPE_PASSWORD }}\n\n      - name: Set release version\n        run: mvn --no-transfer-progress --batch-mode versions:set -DnewVersion=${{ github.event.inputs.nextVersion }}-SNAPSHOT\n\n      - name: Commit & Push changes\n        uses: actions-js/push@master\n        with:\n          github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }}\n          message: \"build: Setting SNAPSHOT version ${{ github.event.inputs.nextVersion }}-SNAPSHOT\"\n          tags: false\n\n      - name: JReleaser release output\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: jreleaser-release\n          path: |\n            out/jreleaser/trace.log\n            out/jreleaser/output.properties"
  },
  {
    "path": ".gitignore",
    "content": "\n# Created by https://www.toptal.com/developers/gitignore/api/java,maven,intellij,eclipse\n# Edit at https://www.toptal.com/developers/gitignore?templates=java,maven,intellij,eclipse\n\n### Eclipse ###\n.metadata\nbin/\ntmp/\n*.tmp\n*.bak\n*.swp\n*~.nib\nlocal.properties\n.settings/\n.loadpath\n.recommenders\n\n# External tool builders\n.externalToolBuilders/\n\n# Locally stored \"Eclipse launch configurations\"\n*.launch\n\n# PyDev specific (Python IDE for Eclipse)\n*.pydevproject\n\n# CDT-specific (C/C++ Development Tooling)\n.cproject\n\n# CDT- autotools\n.autotools\n\n# Java annotation processor (APT)\n.factorypath\n\n# PDT-specific (PHP Development Tools)\n.buildpath\n\n# sbteclipse plugin\n.target\n\n# Tern plugin\n.tern-project\n\n# TeXlipse plugin\n.texlipse\n\n# STS (Spring Tool Suite)\n.springBeans\n\n# Code Recommenders\n.recommenders/\n\n# Annotation Processing\n.apt_generated/\n.apt_generated_test/\n\n# Scala IDE specific (Scala & Java development for Eclipse)\n.cache-main\n.scala_dependencies\n.worksheet\n\n# Uncomment this line if you wish to ignore the project description file.\n# Typically, this file would be tracked if it contains build/dependency configurations:\n#.project\n\n### Eclipse Patch ###\n# Spring Boot Tooling\n.sts4-cache/\n\n### Intellij ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\nbuild/\n.gradle/\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n.idea/*\n.idea\n\n### Intellij Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n*.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n### Java ###\n# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n### Maven ###\ntarget/\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# End of https://www.toptal.com/developers/gitignore/api/java,maven,intellij,eclipse\n\n./jreleaser-cli.jar\n"
  },
  {
    "path": "CITATIONS.md",
    "content": "# Research Citations\n\n\n\n## Directed Feedback Arc Set\n**Title:** Computing a Feedback Arc Set Using PageRank  \n**Authors:** Geladaris, Lionakis, and Tollis  \n**Year:** 2023  \n**Publication:** International Symposium on Graph Drawing and Network Visualization  \n**Links:**  \nhttps://arxiv.org/abs/2208.09234  \nhttps://doi.org/10.1007/978-3-031-22203-0_14  \n**Summary:** The new technique produces solutions that are better than the ones produced by the best previously known heuristics, often reducing the FAS size by more than 50%. \n\n\n## Directed Feedback Vertex Set \n**Title:** Wannabe Bounded Treewidth Graphs Admit a Polynomial Kernel for Directed Feedback Vertex Set    \n**Authors:** Authors: Daniel Lokshtanov, Maadapuzhi-Sridharan Ramanujan, Saket Saurabh, Roohani Sharma, Meirav Zehavi  \n**Year:** 2025  \n**Publication:** ACM Transactions on Computation Theory, Volume 17, Issue 1  \n**Links:** https://doi.org/10.1145/3711669\n\n\n## Tech Debt Prioritization\n**Title:** Prioritizing Design Debt Investment Opportunities  \n**Authors:** Nico Zazworka, Carolyn Seaman, and Forrest Shull  \n**Year:** 2011  \n**Publication:** MTD '11: Proceedings of the 2nd Workshop on Managing Technical Debt  \n**Links:** https://dl.acm.org/doi/10.1145/1985362.1985372"
  },
  {
    "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."
  },
  {
    "path": "README.md",
    "content": "# RefactorFirst\n\nThis tool for Java codebases will help you identify what you should refactor first:\n- God Classes\n- Highly Coupled classes\n- Class Cycles (with cycle images!)\n\nIt scans your Git repository generates a single page application by runing:\n- Cycle analysis on your source code using the [OpenRewrite](https://github.com/openrewrite/rewrite) Java parser and [JGraphT](https://jgrapht.org/)\n- What-if analysis to identify the most optimal relationships in a class cycle to remove\n- PMD's God Class Rule\n- PMD's Coupling Between Objects\n\nCode map viewers are powered by [3D Force Graph](https://vasturiano.github.io/3d-force-graph), [sigma.js](https://www.sigmajs.org/), and [GraphViz DOT](https://graphviz.org/docs/layouts/dot/)\n<br>If there are more than 4000 classes + relationships, a simplified 3D viewer will be used to avoid slowdowns.  Features will be toggleable in the 3D UI in a future release.\n\n## Decomposing and Removing Cycles\nCycle analysis is performed with cutting-edge [Directed Feedback Vertex Set](https://dl.acm.org/doi/10.1145/3711669) and [Directed Feedback Arc Set](https://arxiv.org/abs/2208.09234) \nalgorithms to identify the optimal classes and relationships between classes for removal to get rid of cycles in your codebase.  \nThese algorithms are powerful and will push your CPU to its limits for large codebases, though it does play nice and shouldn't slow your computer down.  \nThese graph algorithms can be used outside RefactorFirst.  \nSee [DIAGRAM.md](./graph-algorithms/src/main/java/org/hjug/feedback/vertex/kernelized/DIAGRAM.md) for the flow of the vertex kernelized algorithm.    \nSee [DIAGRAM.md](./graph-algorithms/src/main/java/org/hjug/feedback/arc/pageRank/DIAGRAM.md) for more details on the arc kernelized algorithm.\n\n\n### How to understand the Relationship Removal Priority table\n\nThe Relationship Removal Priority table shows the most optimal relationships to remove from your codebase to remove all cycles.  \nThe table is sorted by the number of cycles that a relationship exists in and then the change proneness of the classes in the relationship.\n- Classes that should be broken apart / removed from the codebase are bold.  \n- If only one class is bold, the shared functionality should be moved to the non-bold class.  \n- If neither class or both classes are bold, examine both classes and reassess the responsibilities of the classes and refactor to remove the relationship.\n\nTake a look at the [Spring Petclinic REST project sample report](https://rawcdn.githack.com/refactorfirst/RefactorFirst/c46d26211a91ffbe08d4089e04a85ff31eb093c0/spring-petclinic-rest-report.html)!\n\nThe graphs generated in the report will look similar to this one:\n![image info](./RefactorFirst_Sample_Report.png)\n\n## Please Note: Java 11 (or newer) required to run RefactorFirst\n**Java 21 codebase analysis is supported!**\nThe change to require Java 11 is needed to address vulnerability CVE-2023-4759 in JGit \nPlease use a recent JDK release of the Java version you are using.  \nIf you use an old JDK release of your chosen Java version, you may encounter issues during analysis.\n\n\n## There are several ways to run the analysis on your codebase:\n\n### From The Command Line As an HTML Report\nRun the following command from the root of your project (the source code does not need to be built):\n\n```bash\nmvn org.hjug.refactorfirst.plugin:refactor-first-maven-plugin:0.8.0:htmlReport\n```\nView the report at ```target/site/refactor-first-report.html```\n\n### [As Part of GitHub Actions Output](https://github.blog/news-insights/product-news/supercharging-github-actions-with-job-summaries/)\nThis will generate a simplified HTML report (no graphs or images) as the output of a GitHub Action step\n```bash\nmvn -B clean test \\\norg.hjug.refactorfirst.plugin:refactor-first-maven-plugin:0.8.0:simpleHtmlReport \\\n&& echo \"$(cat target/site/refactor-first-report.html)\" >> $GITHUB_STEP_SUMMARY\n```\n\n### As Part of a Build\nAdd the following to your project in the build section.  **showDetails** will show God Class metrics and rankings in the generated table.\n```xml\n<build>\n    <plugins>\n        ...\n        <plugin>\n            <groupId>org.hjug.refactorfirst.plugin</groupId>\n            <artifactId>refactor-first-maven-plugin</artifactId>\n            <version>0.8.0</version>       \n            <!-- optional -->\n            <configuration>\n                <showDetails>false</showDetails>\n            </configuration>\n        </plugin>\n        ...\n    </plugins>\n</build>\n```\n\n### As a Maven Report\nAdd the following to your project in the reports section.   \nA RefactorFirst report will show up in the site report when you run ```mvn site```\n```xml\n<reporting>\n    <plugins>\n        ...\n        <plugin>\n            <groupId>org.hjug.refactorfirst.plugin</groupId>\n            <artifactId>refactor-first-maven-plugin</artifactId>\n            <version>0.8.0</version>       \n        </plugin>\n        ...\n    </plugins>\n</reporting>\n```\n\n## Configuraiton Options\nCare has been taken to use sensible defaults, though if you wish to override these defaults you can specify the following parameters.\nSpecify with -D if running on the command line.  e.g. ```-DbackEdgeAnalysisCount=0 `DanalyzeCycles=false``` or in the configuration section (as in the above examples) if including in a Maven build.\n\n|Option|Action| Default                                                   |\n|------|------|-----------------------------------------------------------|\n|showDetails|Shows God Class metrics| false                                                     |\n|backEdgeAnalysisCount|Number of back edges in a cycle to analyze.  <br>If total number of back edges is greater than the value specified, it analyzes the number of minimum weight edges specified.<br>**If 0 is specified, all back edges will be analyzed**| 50                                                        |\n|analyzeCycles|Analyzes the 10 largest cycles (will be configurable in the future)| true                                                      |\n|minifyHtml|Minifies the generated HTML report.  Only available on ```htmlReport``` and ```simpleHtmlReport``` goals.  May cause issues with large reports.| false                                                     |\n|excludeTests|Exclude test classes from analysis| true                                                      |\n|testSrcDirectory|Excludes classes containing this pattern from analysis| ```src/test``` and ```src\\test```                         |\n|projectName|The name of your project to be displayed on the report| Your Maven project name                                   |\n|projectVersion|The version of your project to be displayed on the report| Your Maven project version                                |\n|outputDirectory|The location the project report will be written| ```${projectDir}/target/site/refactor-first-report.html``` \n\n\n### Seeing Errors?\n\nIf you see an error similar to\n```\n Execution default-site of goal org.apache.maven.plugins:maven-site-plugin:3.3:site failed: A required class was missing while executing org.apache.maven.plugins:maven-site-plugin:3.3:site: org/apache/maven/doxia/siterenderer/DocumentContent\n```\nyou will need to add the following to your pom.xml:\n```xml\n  <build>\n    <plugins>        \n      <plugin>\n        <groupId>org.apache.maven.plugins</groupId>\n        <artifactId>maven-site-plugin</artifactId>\n        <version>3.12.1</version>\n      </plugin>\n      <plugin>\n        <groupId>org.apache.maven.plugins</groupId>\n        <artifactId>maven-project-info-reports-plugin</artifactId>\n        <version>3.4.5</version>\n      </plugin>\n    </plugins>\n  </build>\n```\n\n\n## But I'm using Gradle / my project layout isn't typical!\nI would like to create a Gradle plugin and (possibly) support non-conventional projects in the future, but in the meantime you can create a dummy POM file in the same directory as your .git directory:\n\n```xml\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n \n  <groupId>com.mycompany.app</groupId>\n  <artifactId>my-app</artifactId>\n  <version>1.0-SNAPSHOT</version>\n</project>\n```\nand then (assuming Maven is installed) run\n\n```bash\nmvn org.hjug.refactorfirst.plugin:refactor-first-maven-plugin:0.8.0:htmlReport\n```\n\n## Viewing the Report\nView the report at ```target/site/refactor-first-report.html```   \nOnce the plugin finishes executing (it may take a while for a large / old codebase), open the file **target/site/refactor-first-report.html** in the root of the project.  It will contain a graph similar to the one above, and a table that lists God classes in the recommended order that they should be refactored.  The classes in the top left of the graph are the easiest to refactor while also having the biggest positive impact to team productivity.  \nIf highly coupled classes are detected, a graph and table listing Highly Coupled Classes in will be generated.\n\n## I have the report.  Now What???\nWork with your Product Owner to prioritize the technical debt that has been identified.  It may help to explain it as hidden negative value that is slowing team porductivity.  \nIf you have IntelliJ Ultimate, you can install the [Method Reference Diagram](https://plugins.jetbrains.com/plugin/7996-java-method-reference-diagram) plugin to help you determine how the identified God classes and Highly Coupled classes can be refactored.\n\n\n## Additional Details\nThis plugin will work on both single module and multi-module Maven projects that have a typical Maven project layout.\n \nThis tool is based on the paper **[Prioritizing Design Debt Investment Opportunities](https://dl.acm.org/doi/10.1145/1985362.1985372)** by Nico Zazworka, Carolyn Seaman, and Forrest Shull.  The presentation based on the paper is available at https://resources.sei.cmu.edu/asset_files/Presentation/2011_017_001_516911.pdf \n\n## Limitations\n* My time.  This is a passion project and is developed in my spare time.\n\n## Feedback and Collaboration Welcome\nThere is still much to be done.  Your feedback and collaboration would be greatly appreciated in the form of feature requests, bug submissions, and PRs.  \nIf you find this plugin useful, please star this repository and share with your friends & colleagues and on social media.\n\n## Future Plans\n* Improve class cycle analysis\n* Add a Gradle plugin.\n* Incorporate Unit Test coverage metrics to quickly identify the safety of refactoring classes.\n* Incorporate bug counts per class to the Impact (Y-Axis) calculation.\n* Incorporate more disharmonies from Object Oriented Metrics In Practice (Lanza and Marinescu, 2004).\n\n## Note:\nIf you are a user of Version 0.1.0 or 0.1.1, you may notice that the list of God classes found by the plugin has changed.  This is due to changes in PMD.\n\n# Thank You!  Enjoy!\n"
  },
  {
    "path": "change-proneness-ranker/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.changepronenessranker</groupId>\n    <artifactId>change-proneness-ranker</artifactId>\n\n    <name>RefactorFirst Change Proneness Ranker</name>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>${project.basedir}/src/test/resources</directory>\n            </testResource>\n        </testResources>\n    </build>\n\n    <dependencies>\n\n        <dependency>\n            <groupId>org.eclipse.jgit</groupId>\n            <artifactId>org.eclipse.jgit</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.testresources</groupId>\n            <artifactId>test-resources</artifactId>\n        </dependency>\n\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "change-proneness-ranker/src/main/java/org/hjug/git/ChangePronenessRanker.java",
    "content": "package org.hjug.git;\n\nimport java.io.IOException;\nimport java.util.*;\nimport lombok.extern.slf4j.Slf4j;\nimport org.eclipse.jgit.api.errors.GitAPIException;\n\n@Slf4j\npublic class ChangePronenessRanker {\n\n    private final TreeMap<Integer, Integer> changeCountsByTimeStamps = new TreeMap<>();\n    private final Map<String, ScmLogInfo> cachedScmLogInfos = new HashMap<>();\n\n    public ChangePronenessRanker(GitLogReader repositoryLogReader) {\n        try {\n            log.info(\"Capturing change count based on commit timestamps\");\n            changeCountsByTimeStamps.putAll(repositoryLogReader.captureChangeCountByCommitTimestamp());\n        } catch (IOException | GitAPIException e) {\n            log.error(\"Error reading from repository: {}\", e.getMessage());\n        }\n    }\n\n    public void rankChangeProneness(List<ScmLogInfo> scmLogInfos) {\n        for (ScmLogInfo scmLogInfo : scmLogInfos) {\n            if (!cachedScmLogInfos.containsKey(scmLogInfo.getPath())) {\n                int commitsInRepositorySinceCreation =\n                        changeCountsByTimeStamps.tailMap(scmLogInfo.getEarliestCommit()).values().stream()\n                                .mapToInt(i -> i)\n                                .sum();\n\n                scmLogInfo.setChangeProneness((float) scmLogInfo.getCommitCount() / commitsInRepositorySinceCreation);\n                cachedScmLogInfos.put(scmLogInfo.getPath(), scmLogInfo);\n            } else {\n                scmLogInfo.setChangeProneness(\n                        cachedScmLogInfos.get(scmLogInfo.getPath()).getChangeProneness());\n            }\n        }\n\n        scmLogInfos.sort(Comparator.comparing(ScmLogInfo::getChangeProneness));\n\n        int rank = 0;\n        for (ScmLogInfo scmLogInfo : scmLogInfos) {\n            scmLogInfo.setChangePronenessRank(++rank);\n        }\n    }\n}\n"
  },
  {
    "path": "change-proneness-ranker/src/main/java/org/hjug/git/GitLogReader.java",
    "content": "package org.hjug.git;\n\nimport java.io.*;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.stream.IntStream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.api.errors.GitAPIException;\nimport org.eclipse.jgit.diff.DiffEntry;\nimport org.eclipse.jgit.diff.DiffFormatter;\nimport org.eclipse.jgit.lib.*;\nimport org.eclipse.jgit.revwalk.*;\nimport org.eclipse.jgit.storage.file.FileRepositoryBuilder;\nimport org.eclipse.jgit.treewalk.CanonicalTreeParser;\nimport org.eclipse.jgit.treewalk.TreeWalk;\nimport org.eclipse.jgit.util.io.NullOutputStream;\n\n@Slf4j\npublic class GitLogReader implements AutoCloseable {\n\n    static final String JAVA_FILE_TYPE = \".java\";\n\n    private Repository gitRepository;\n\n    private Git git;\n\n    public GitLogReader() {}\n\n    public GitLogReader(File basedir) throws IOException {\n        FileRepositoryBuilder repositoryBuilder = new FileRepositoryBuilder().findGitDir(basedir);\n        String gitIndexFileEnvVariable = System.getenv(\"GIT_INDEX_FILE\");\n        if (Objects.nonNull(gitIndexFileEnvVariable)\n                && !gitIndexFileEnvVariable.trim().isEmpty()) {\n            log.debug(\"Setting Index File based on Env Variable GIT_INDEX_FILE {}\", gitIndexFileEnvVariable);\n            repositoryBuilder = repositoryBuilder.setIndexFile(new File(gitIndexFileEnvVariable));\n        }\n\n        git = Git.open(repositoryBuilder.getGitDir());\n        gitRepository = git.getRepository();\n    }\n\n    GitLogReader(Git git) {\n        this.git = git;\n        gitRepository = git.getRepository();\n    }\n\n    @Override\n    public void close() throws Exception {\n        git.close();\n    }\n\n    public File getGitDir(File basedir) {\n        FileRepositoryBuilder repositoryBuilder = new FileRepositoryBuilder().findGitDir(basedir);\n        return repositoryBuilder.getGitDir();\n    }\n\n    // log --follow implementation may be worth adopting in the future\n    // https://github.com/spearce/jgit/blob/master/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevWalkTextBuiltin.java\n\n    /**\n     * Returns the number of commits and earliest commit for a given path\n     * TODO: Move to a different class???\n     *\n     * @param path\n     * @return a LogInfo object\n     * @throws GitAPIException\n     */\n    public ScmLogInfo fileLog(String path) throws GitAPIException, IOException {\n        ObjectId branchId = gitRepository.resolve(\"HEAD\");\n        Iterable<RevCommit> revCommits = git.log().add(branchId).addPath(path).call();\n\n        int commitCount = 0;\n        int earliestCommit = Integer.MAX_VALUE;\n        int mostRecentCommit = 0;\n\n        for (RevCommit revCommit : revCommits) {\n            int commitTime = revCommit.getCommitTime();\n            if (commitCount == 0) {\n                mostRecentCommit = commitTime;\n            }\n            if (commitTime < earliestCommit) {\n                earliestCommit = commitTime;\n            }\n            commitCount++;\n        }\n\n        if (commitCount == 0) {\n            return new ScmLogInfo(path, null, earliestCommit, earliestCommit, commitCount);\n        }\n\n        return new ScmLogInfo(path, null, earliestCommit, mostRecentCommit, commitCount);\n    }\n\n    // based on https://stackoverflow.com/questions/27361538/how-to-show-changes-between-commits-with-jgit\n    public TreeMap<Integer, Integer> captureChangeCountByCommitTimestamp() throws IOException, GitAPIException {\n\n        TreeMap<Integer, Integer> changesByCommitTimestamp = new TreeMap<>();\n\n        ObjectId branchId = gitRepository.resolve(\"HEAD\");\n        List<RevCommit> commitList = new ArrayList<>();\n        git.log().add(branchId).call().forEach(commitList::add);\n\n        if (commitList.isEmpty()) {\n            return changesByCommitTimestamp;\n        }\n\n        // Handle first / initial commit\n        changesByCommitTimestamp.putAll(walkFirstCommit(commitList.get(commitList.size() - 1)));\n\n        if (commitList.size() < 2) {\n            return changesByCommitTimestamp;\n        }\n\n        // Process adjacent commit pairs in parallel; each pair is independent\n        ConcurrentMap<Integer, Integer> concurrentResults = new ConcurrentHashMap<>();\n        IntStream.range(0, commitList.size() - 1).parallel().forEach(i -> {\n            RevCommit newer = commitList.get(i);\n            RevCommit older = commitList.get(i + 1);\n            try {\n                int count = 0;\n                for (DiffEntry entry : getDiffEntries(newer, older)) {\n                    if (entry.getNewPath().endsWith(JAVA_FILE_TYPE)\n                            || entry.getOldPath().endsWith(JAVA_FILE_TYPE)) {\n                        count++;\n                    }\n                }\n                if (count > 0) {\n                    concurrentResults.put(newer.getCommitTime(), count);\n                }\n            } catch (IOException e) {\n                log.error(\"Error getting diff entries: {}\", e.getMessage());\n            }\n        });\n\n        changesByCommitTimestamp.putAll(concurrentResults);\n        return changesByCommitTimestamp;\n    }\n\n    private List<DiffEntry> getDiffEntries(RevCommit newCommit, RevCommit oldCommit) throws IOException {\n        try (ObjectReader reader = gitRepository.newObjectReader();\n                DiffFormatter df = new DiffFormatter(NullOutputStream.INSTANCE)) {\n            df.setRepository(gitRepository);\n            CanonicalTreeParser oldTreeIter = new CanonicalTreeParser();\n            oldTreeIter.reset(reader, newCommit.getTree());\n            CanonicalTreeParser newTreeIter = new CanonicalTreeParser();\n            newTreeIter.reset(reader, oldCommit.getTree());\n            return df.scan(oldTreeIter, newTreeIter);\n        }\n    }\n\n    Map<Integer, Integer> walkFirstCommit(RevCommit firstCommit) throws IOException {\n        Map<Integer, Integer> changesByCommitTimestamp = new TreeMap<>();\n        int firstCommitCount = 0;\n        ObjectId treeId = firstCommit.getTree();\n        try (TreeWalk treeWalk = new TreeWalk(gitRepository)) {\n            treeWalk.setRecursive(false);\n            treeWalk.reset(treeId);\n            while (treeWalk.next()) {\n                if (treeWalk.isSubtree()) {\n                    treeWalk.enterSubtree();\n                } else {\n                    if (treeWalk.getPathString().endsWith(JAVA_FILE_TYPE)) {\n                        firstCommitCount++;\n                    }\n                }\n            }\n        }\n\n        if (firstCommitCount > 0) {\n            changesByCommitTimestamp.put(firstCommit.getCommitTime(), firstCommitCount);\n        }\n\n        return changesByCommitTimestamp;\n    }\n}\n"
  },
  {
    "path": "change-proneness-ranker/src/main/java/org/hjug/git/ScmLogInfo.java",
    "content": "package org.hjug.git;\n\nimport lombok.Data;\n\n@Data\npublic class ScmLogInfo {\n\n    private String path;\n    private String className;\n    private int earliestCommit;\n    private int mostRecentCommit;\n    private int commitCount;\n    private float changeProneness;\n    private int changePronenessRank;\n\n    public ScmLogInfo(String path, String className, int earliestCommit, int mostRecentCommit, int commitCount) {\n        this.path = path;\n        this.className = className;\n        this.earliestCommit = earliestCommit;\n        this.mostRecentCommit = mostRecentCommit;\n        this.commitCount = commitCount;\n    }\n}\n"
  },
  {
    "path": "change-proneness-ranker/src/test/java/org/hjug/git/ChangePronenessRankerTest.java",
    "content": "package org.hjug.git;\n\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\nimport java.io.IOException;\nimport java.util.*;\nimport org.eclipse.jgit.api.errors.GitAPIException;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass ChangePronenessRankerTest {\n\n    private ChangePronenessRanker changePronenessRanker;\n    private GitLogReader repositoryLogReader;\n\n    @BeforeEach\n    public void setUp() {\n        repositoryLogReader = mock(GitLogReader.class);\n    }\n\n    // TODO: this should probably be a cucumber test\n    @Test\n    void testChangePronenessCalculation() throws IOException, GitAPIException {\n        ScmLogInfo scmLogInfo = new ScmLogInfo(\"path\", null, 1595275997, 0, 1);\n\n        TreeMap<Integer, Integer> commitsWithChangeCounts = new TreeMap<>();\n        commitsWithChangeCounts.put(scmLogInfo.getEarliestCommit(), scmLogInfo.getCommitCount());\n        commitsWithChangeCounts.put(scmLogInfo.getEarliestCommit() + 5 * 60, 3);\n        commitsWithChangeCounts.put(scmLogInfo.getEarliestCommit() + 10 * 60, 3);\n\n        when(repositoryLogReader.captureChangeCountByCommitTimestamp()).thenReturn(commitsWithChangeCounts);\n\n        changePronenessRanker = new ChangePronenessRanker(repositoryLogReader);\n        List<ScmLogInfo> scmLogInfos = new ArrayList<>();\n        scmLogInfos.add(scmLogInfo);\n        changePronenessRanker.rankChangeProneness(scmLogInfos);\n\n        // 1 commit of a class we're interested in, 6 commits of other files after it\n        Assertions.assertEquals((float) 1 / 7, scmLogInfo.getChangeProneness(), 0.1);\n    }\n\n    @Test\n    void testRankChangeProneness() throws IOException, GitAPIException {\n        // more recent commit\n        ScmLogInfo newerCommit = new ScmLogInfo(\"file1\", null, 1595275997, 0, 1);\n\n        TreeMap<Integer, Integer> commitsWithChangeCounts = new TreeMap<>();\n        commitsWithChangeCounts.put(newerCommit.getEarliestCommit(), newerCommit.getCommitCount());\n        commitsWithChangeCounts.put(newerCommit.getEarliestCommit() + 5 * 60, 3);\n        commitsWithChangeCounts.put(newerCommit.getEarliestCommit() + 10 * 60, 3);\n\n        // older commit\n        ScmLogInfo olderCommit = new ScmLogInfo(\"file2\", null, 1595175997, 0, 1);\n\n        commitsWithChangeCounts.put(olderCommit.getEarliestCommit(), olderCommit.getCommitCount());\n        commitsWithChangeCounts.put(olderCommit.getEarliestCommit() + 5 * 60, 5);\n        commitsWithChangeCounts.put(olderCommit.getEarliestCommit() + 10 * 60, 5);\n\n        when(repositoryLogReader.captureChangeCountByCommitTimestamp()).thenReturn(commitsWithChangeCounts);\n        changePronenessRanker = new ChangePronenessRanker(repositoryLogReader);\n\n        List<ScmLogInfo> scmLogInfos = new ArrayList<>();\n        scmLogInfos.add(newerCommit);\n        scmLogInfos.add(olderCommit);\n        changePronenessRanker.rankChangeProneness(scmLogInfos);\n\n        // ranks higher since fewer commits since initial commit\n        Assertions.assertEquals(2, newerCommit.getChangePronenessRank());\n        // ranks lower since there have been more commits since initial commit\n        Assertions.assertEquals(1, olderCommit.getChangePronenessRank());\n    }\n}\n"
  },
  {
    "path": "change-proneness-ranker/src/test/java/org/hjug/git/GitLogReaderTest.java",
    "content": "package org.hjug.git;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport java.io.*;\nimport java.util.*;\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.api.errors.GitAPIException;\nimport org.eclipse.jgit.lib.Repository;\nimport org.eclipse.jgit.revwalk.RevCommit;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\n\npublic class GitLogReaderTest {\n    // Borrowed bits and pieces from\n    // https://gist.github.com/rherrmann/0c682ea327862cb6847704acf90b1d5d\n\n    @TempDir\n    public File tempFolder;\n\n    private Git git;\n    private Repository repository;\n\n    @BeforeEach\n    public void setUp() throws GitAPIException {\n        git = Git.init().setDirectory(tempFolder).call();\n        repository = git.getRepository();\n    }\n\n    @AfterEach\n    public void tearDown() {\n        repository.close();\n    }\n\n    @Test\n    void testFileLog() throws IOException, GitAPIException, InterruptedException {\n        // This path works when referencing the full Tobago repository\n        // String filePath = \"tobago-core/src/main/java/org/apache/myfaces/tobago/facelets/AttributeHandler.java\";\n\n        GitLogReader gitLogReader = new GitLogReader(git);\n\n        String attributeHandler = \"AttributeHandler.java\";\n        InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(attributeHandler);\n        writeFile(attributeHandler, convertInputStreamToString(resourceAsStream));\n\n        git.add().addFilepattern(\".\").call();\n        RevCommit firstCommit = git.commit().setMessage(\"message\").call();\n\n        // Sleeping for one second to guarantee commits have different time stamps\n        Thread.sleep(1000);\n\n        // write contents of updated file to original file\n        InputStream resourceAsStream2 = getClass().getClassLoader().getResourceAsStream(\"AttributeHandler2.java\");\n        writeFile(attributeHandler, convertInputStreamToString(resourceAsStream2));\n\n        git.add().addFilepattern(\".\").call();\n        RevCommit secondCommit = git.commit().setMessage(\"message\").call();\n\n        ScmLogInfo scmLogInfo = gitLogReader.fileLog(attributeHandler);\n\n        Assertions.assertEquals(2, scmLogInfo.getCommitCount());\n        Assertions.assertEquals(firstCommit.getCommitTime(), scmLogInfo.getEarliestCommit());\n        Assertions.assertEquals(secondCommit.getCommitTime(), scmLogInfo.getMostRecentCommit());\n    }\n\n    @Test\n    void testWalkFirstCommit() throws IOException, GitAPIException {\n        GitLogReader gitLogReader = new GitLogReader(git);\n\n        String attributeHandler = \"AttributeHandler.java\";\n        InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(attributeHandler);\n        writeFile(attributeHandler, convertInputStreamToString(resourceAsStream));\n        git.add().addFilepattern(\".\").call();\n        RevCommit commit = git.commit().setMessage(\"message\").call();\n\n        Map<Integer, Integer> result = gitLogReader.walkFirstCommit(commit);\n\n        Assertions.assertTrue(result.containsKey(commit.getCommitTime()));\n        Assertions.assertEquals(1, result.get(commit.getCommitTime()).intValue());\n    }\n\n    @Test\n    void testCaptureChangCountByCommitTimestamp() throws Exception {\n        GitLogReader gitLogReader = new GitLogReader(git);\n\n        String attributeHandler = \"AttributeHandler.java\";\n        InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(attributeHandler);\n        writeFile(attributeHandler, convertInputStreamToString(resourceAsStream));\n\n        git.add().addFilepattern(\".\").call();\n        RevCommit firstCommit = git.commit().setMessage(\"message\").call();\n\n        // Sleeping for one second to guarantee commits have different time stamps\n        Thread.sleep(1000);\n\n        // write contents of updated file to original file\n        InputStream resourceAsStream2 = getClass().getClassLoader().getResourceAsStream(\"AttributeHandler2.java\");\n        writeFile(attributeHandler, convertInputStreamToString(resourceAsStream2));\n\n        InputStream resourceAsStream3 = getClass().getClassLoader().getResourceAsStream(\"Attributes.java\");\n        writeFile(\"Attributes.java\", convertInputStreamToString(resourceAsStream3));\n\n        git.add().addFilepattern(\".\").call();\n        RevCommit secondCommit = git.commit().setMessage(\"message\").call();\n\n        Map<Integer, Integer> commitCounts = gitLogReader.captureChangeCountByCommitTimestamp();\n\n        Assertions.assertEquals(1, commitCounts.get(firstCommit.getCommitTime()).intValue());\n        Assertions.assertEquals(\n                2, commitCounts.get(secondCommit.getCommitTime()).intValue());\n    }\n\n    private void writeFile(String name, String content) throws IOException {\n        File file = new File(git.getRepository().getWorkTree(), name);\n        try (FileOutputStream outputStream = new FileOutputStream(file)) {\n            outputStream.write(content.getBytes(UTF_8));\n        }\n    }\n\n    private String convertInputStreamToString(InputStream inputStream) throws IOException {\n        ByteArrayOutputStream result = new ByteArrayOutputStream();\n        byte[] buffer = new byte[1024];\n        int length;\n        while ((length = inputStream.read(buffer)) != -1) {\n            result.write(buffer, 0, length);\n        }\n        return result.toString(\"UTF-8\");\n    }\n}\n"
  },
  {
    "path": "cli/.gitignore",
    "content": "target/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### IntelliJ IDEA ###\n.idea/modules.xml\n.idea/jarRepositories.xml\n.idea/compiler.xml\n.idea/libraries/\n*.iws\n*.iml\n*.ipr\n\n### Eclipse ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n\n### Mac OS ###\n.DS_Store"
  },
  {
    "path": "cli/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <packaging>jar</packaging>\n\n    <groupId>org.hjug.refactorfirst.report</groupId>\n    <artifactId>cli</artifactId>\n\n    <name>RefactorFirst CLI</name>\n\n    <properties>\n        <maven.compiler.source>11</maven.compiler.source>\n        <maven.compiler.target>11</maven.compiler.target>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>info.picocli</groupId>\n            <artifactId>picocli</artifactId>\n            <version>4.7.4</version>\n        </dependency>\n        <dependency>\n            <groupId>org.hjug.refactorfirst.report</groupId>\n            <artifactId>report</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-simple</artifactId>\n        </dependency>\n        <!-- Needed to suppress CVE-2023-2976 in maven-core:3.9.9 -->\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven</groupId>\n            <artifactId>maven-core</artifactId>\n        </dependency>\n\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>3.5.0</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <mainClass>org.hjug.refactorfirst.Main</mainClass>\n                                </transformer>\n                            </transformers>\n                            <filters>\n                                <filter>\n                                    <artifact>*:*</artifact>\n                                    <excludes>\n                                        <exclude>META-INF/*.SF</exclude>\n                                        <exclude>META-INF/*.DSA</exclude>\n                                        <exclude>META-INF/*.RSA</exclude>\n                                    </excludes>\n                                </filter>\n                            </filters>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>org.skife.maven</groupId>\n                <artifactId>really-executable-jar-maven-plugin</artifactId>\n                <version>2.1.1</version>\n                <configuration>\n                    <programFile>rf</programFile>\n                </configuration>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>really-executable-jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>"
  },
  {
    "path": "cli/src/main/java/org/hjug/refactorfirst/Main.java",
    "content": "package org.hjug.refactorfirst;\n\nimport picocli.CommandLine;\n\npublic class Main {\n    public static void main(String[] args) {\n        int exitCode = new CommandLine(new ReportCommand())\n                .setCaseInsensitiveEnumValuesAllowed(true)\n                .execute(args);\n        System.exit(exitCode);\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/org/hjug/refactorfirst/ReportCommand.java",
    "content": "package org.hjug.refactorfirst;\n\nimport static picocli.CommandLine.Option;\n\nimport java.io.File;\nimport java.io.FileReader;\nimport java.util.concurrent.Callable;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.maven.model.Model;\nimport org.apache.maven.model.io.xpp3.MavenXpp3Reader;\nimport org.apache.maven.project.MavenProject;\nimport org.hjug.refactorfirst.report.CsvReport;\nimport org.hjug.refactorfirst.report.HtmlReport;\nimport org.hjug.refactorfirst.report.SimpleHtmlReport;\nimport org.hjug.refactorfirst.report.json.JsonReportExecutor;\nimport picocli.CommandLine.Command;\n\n@Command(mixinStandardHelpOptions = true, description = \"Generate a report\")\n@Slf4j\npublic class ReportCommand implements Callable<Integer> {\n\n    @Option(\n            names = {\"-d\", \"--details\"},\n            defaultValue = \"false\",\n            description = \"Show detailed report\")\n    private boolean showDetails;\n\n    @Option(\n            names = {\"-eac\", \"--edge-analysis-count\"},\n            defaultValue = \"50\",\n            description = \"Back Edge Analysis Count\")\n    protected int backEdgeAnalysisCount;\n\n    @Option(\n            names = {\"-c\", \"--analyze-cycles\"},\n            defaultValue = \"true\",\n            description = \"Analyze Cycles\")\n    private boolean analyzeCycles;\n\n    @Option(\n            names = {\"-m\", \"--minify-html\"},\n            defaultValue = \"false\",\n            description = \"Minify HTML output\")\n    private boolean minifiyHtml;\n\n    @Option(\n            names = {\"-xt\", \"--exclude-tests\"},\n            defaultValue = \"true\",\n            description = \"Exclude tests from analysis\")\n    private boolean excludeTests;\n\n    /**\n     * The test source directory containing test class sources.\n     */\n    @Option(\n            names = {\"-tsd\", \"--output\"},\n            description =\n                    \"Test source directory.  Defaults to test/src or test\\\\src based on your OS.  Default is intentionally generic.\")\n    private String testSourceDirectory;\n\n    @Option(\n            names = {\"-p\", \"--project\"},\n            description = \"Project name\")\n    private String projectName;\n\n    @Option(\n            names = {\"-v\", \"--version\"},\n            description = \"Project version\")\n    private String projectVersion;\n\n    @Option(\n            names = {\"-o\", \"--output\"},\n            defaultValue = \".\",\n            description = \"Output directory\")\n    private String outputDirectory;\n\n    @Option(\n            names = {\"-b\", \"--base-dir\"},\n            defaultValue = \".\",\n            description = \"Base directory of the project\")\n    private File baseDir;\n\n    @Option(\n            names = {\"-t\", \"--type\"},\n            description = \"Report type: ${COMPLETION-CANDIDATES}\",\n            defaultValue = \"HTML\")\n    private ReportType reportType;\n\n    @Override\n    public Integer call() {\n\n        // TODO: add support for inferring arguments from gradle properties\n        inferArgumentsFromMavenProject();\n        populateDefaultArguments();\n        switch (reportType) {\n            case SIMPLE_HTML:\n                SimpleHtmlReport simpleHtmlReport = new SimpleHtmlReport();\n                simpleHtmlReport.execute(\n                        backEdgeAnalysisCount,\n                        analyzeCycles,\n                        showDetails,\n                        minifiyHtml,\n                        excludeTests,\n                        testSourceDirectory,\n                        projectName,\n                        projectVersion,\n                        baseDir,\n                        outputDirectory);\n                return 0;\n            case HTML:\n                HtmlReport htmlReport = new HtmlReport();\n                htmlReport.execute(\n                        backEdgeAnalysisCount,\n                        analyzeCycles,\n                        showDetails,\n                        minifiyHtml,\n                        excludeTests,\n                        testSourceDirectory,\n                        projectName,\n                        projectVersion,\n                        baseDir,\n                        outputDirectory);\n                return 0;\n            case JSON:\n                JsonReportExecutor jsonReportExecutor = new JsonReportExecutor();\n                jsonReportExecutor.execute(baseDir, outputDirectory);\n                return 0;\n            case CSV:\n                CsvReport csvReport = new CsvReport();\n                csvReport.execute(showDetails, projectName, projectVersion, outputDirectory, baseDir);\n                return 0;\n        }\n\n        return 0;\n    }\n\n    private void populateDefaultArguments() {\n        if (projectName == null || projectName.isEmpty()) {\n            projectName = \"my-project\";\n        }\n        if (projectVersion == null || projectVersion.isEmpty()) {\n            projectVersion = \"0.0.0\";\n        }\n    }\n\n    private void inferArgumentsFromMavenProject() {\n        if (baseDir.isDirectory()) {\n            File[] potentialPomFiles = baseDir.listFiles(f -> f.getName().equals(\"pom.xml\"));\n            File pomFile = null;\n            if (potentialPomFiles != null && potentialPomFiles.length > 0) {\n                pomFile = potentialPomFiles[0];\n            }\n            if (pomFile != null) {\n                Model model;\n                FileReader reader;\n                MavenXpp3Reader mavenreader = new MavenXpp3Reader();\n                try {\n                    reader = new FileReader(pomFile);\n                    model = mavenreader.read(reader);\n                    model.setPomFile(pomFile);\n                } catch (Exception ex) {\n                    log.info(\"Unable to infer arguments from pom file\");\n                    return;\n                }\n                MavenProject project = new MavenProject(model);\n\n                // only override project name and version if they are not set\n                if (projectName == null || projectName.isEmpty()) {\n                    projectName = project.getName();\n                }\n                if (projectVersion == null || projectVersion.isEmpty()) {\n                    projectVersion = project.getVersion();\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/org/hjug/refactorfirst/ReportType.java",
    "content": "package org.hjug.refactorfirst;\n\npublic enum ReportType {\n    SIMPLE_HTML,\n    HTML,\n    JSON,\n    CSV;\n}\n"
  },
  {
    "path": "codebase-graph-builder/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.codebasegraphbuilder</groupId>\n    <artifactId>codebase-graph-builder</artifactId>\n\n    <name>RefactorFirst Codebase Graph Builder</name>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.openrewrite.recipe</groupId>\n                <artifactId>rewrite-recipe-bom</artifactId>\n                <version>3.4.0</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jgrapht</groupId>\n            <artifactId>jgrapht-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.openrewrite</groupId>\n            <artifactId>rewrite-java-21</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.openrewrite</groupId>\n            <artifactId>rewrite-java-17</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.openrewrite</groupId>\n            <artifactId>rewrite-java-11</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.openrewrite</groupId>\n            <artifactId>rewrite-java</artifactId>\n        </dependency>\n\n        <!--gizmo 1.0.11, used by rewrite-core has a CVSS score > 8.0 -->\n        <dependency>\n            <groupId>io.quarkus.gizmo</groupId>\n            <artifactId>gizmo</artifactId>\n            <version>1.9.0</version>\n        </dependency>\n        <dependency>\n            <groupId>org.openrewrite</groupId>\n            <artifactId>rewrite-core</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/CodebaseGraphDTO.java",
    "content": "package org.hjug.graphbuilder;\n\nimport java.util.Map;\nimport lombok.Data;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n@Data\npublic class CodebaseGraphDTO {\n\n    private final Graph<String, DefaultWeightedEdge> classReferencesGraph;\n    private final Graph<String, DefaultWeightedEdge> packageReferencesGraph;\n    // used for looking up files where classes reside\n    private final Map<String, String> classToSourceFilePathMapping;\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/DependencyCollector.java",
    "content": "package org.hjug.graphbuilder;\n\npublic interface DependencyCollector {\n\n    /**\n     * Records a dependency from one class to another\n     *\n     * @param fromClassFqn The fully qualified name of the class that depends on another\n     * @param toClassFqn The fully qualified name of the class being depended upon\n     */\n    void addClassDependency(String fromClassFqn, String toClassFqn);\n\n    /**\n     * Records a dependency from one package to another\n     *\n     * @param fromPackageName The package that depends on another\n     * @param toPackageName The package being depended upon\n     */\n    void addPackageDependency(String fromPackageName, String toPackageName);\n\n    /**\n     * Records the source file location for a class\n     *\n     * @param classFqn The fully qualified name of the class\n     * @param sourceFilePath The path to the source file containing the class\n     */\n    void recordClassLocation(String classFqn, String sourceFilePath);\n\n    /**\n     * Registers a package as being part of the codebase\n     *\n     * @param packageName The package name to register\n     */\n    void registerPackage(String packageName);\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphBuilderConfig.java",
    "content": "package org.hjug.graphbuilder;\n\nimport lombok.Builder;\nimport lombok.Value;\n\n@Value\n@Builder\npublic class GraphBuilderConfig {\n\n    @Builder.Default\n    boolean excludeTests = true;\n\n    @Builder.Default\n    String testSourceDirectory = \"src/test\";\n\n    public static GraphBuilderConfig defaultConfig() {\n        return GraphBuilderConfig.builder().build();\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/GraphDependencyCollector.java",
    "content": "package org.hjug.graphbuilder;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport lombok.Getter;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\npublic class GraphDependencyCollector implements DependencyCollector {\n\n    @Getter\n    private final Graph<String, DefaultWeightedEdge> classReferencesGraph;\n\n    @Getter\n    private final Graph<String, DefaultWeightedEdge> packageReferencesGraph;\n\n    @Getter\n    private final Set<String> packagesInCodebase = new HashSet<>();\n\n    public GraphDependencyCollector(\n            Graph<String, DefaultWeightedEdge> classReferencesGraph,\n            Graph<String, DefaultWeightedEdge> packageReferencesGraph) {\n        this.classReferencesGraph = classReferencesGraph;\n        this.packageReferencesGraph = packageReferencesGraph;\n    }\n\n    @Override\n    public void addClassDependency(String fromClassFqn, String toClassFqn) {\n        if (fromClassFqn.equals(toClassFqn)) {\n            return;\n        }\n\n        classReferencesGraph.addVertex(fromClassFqn);\n        classReferencesGraph.addVertex(toClassFqn);\n\n        if (!classReferencesGraph.containsEdge(fromClassFqn, toClassFqn)) {\n            classReferencesGraph.addEdge(fromClassFqn, toClassFqn);\n        } else {\n            DefaultWeightedEdge edge = classReferencesGraph.getEdge(fromClassFqn, toClassFqn);\n            classReferencesGraph.setEdgeWeight(edge, classReferencesGraph.getEdgeWeight(edge) + 1);\n        }\n    }\n\n    @Override\n    public void addPackageDependency(String fromPackageName, String toPackageName) {\n        if (fromPackageName.equals(toPackageName)) {\n            return;\n        }\n\n        packageReferencesGraph.addVertex(fromPackageName);\n        packageReferencesGraph.addVertex(toPackageName);\n\n        if (!packageReferencesGraph.containsEdge(fromPackageName, toPackageName)) {\n            packageReferencesGraph.addEdge(fromPackageName, toPackageName);\n        } else {\n            DefaultWeightedEdge edge = packageReferencesGraph.getEdge(fromPackageName, toPackageName);\n            packageReferencesGraph.setEdgeWeight(edge, packageReferencesGraph.getEdgeWeight(edge) + 1);\n        }\n    }\n\n    @Override\n    public void recordClassLocation(String classFqn, String sourceFilePath) {\n        // This will be handled by JavaVisitor which maintains the mapping\n    }\n\n    @Override\n    public void registerPackage(String packageName) {\n        packagesInCodebase.add(packageName);\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/JavaGraphBuilder.java",
    "content": "package org.hjug.graphbuilder;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.graphbuilder.visitor.JavaMethodDeclarationVisitor;\nimport org.hjug.graphbuilder.visitor.JavaVariableTypeVisitor;\nimport org.hjug.graphbuilder.visitor.JavaVisitor;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedWeightedGraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\n@Slf4j\npublic class JavaGraphBuilder {\n\n    /**\n     * Given a java source directory, return a CodebaseGraphDTO using default configuration\n     *\n     * @param srcDirectory The source directory to analyze\n     * @param excludeTests Whether to exclude test files\n     * @param testSourceDirectory The test source directory pattern to exclude\n     * @return CodebaseGraphDTO\n     * @throws IOException\n     */\n    public CodebaseGraphDTO getCodebaseGraphDTO(String srcDirectory, boolean excludeTests, String testSourceDirectory)\n            throws IOException {\n        GraphBuilderConfig config = GraphBuilderConfig.builder()\n                .excludeTests(excludeTests)\n                .testSourceDirectory(testSourceDirectory)\n                .build();\n        return getCodebaseGraphDTO(srcDirectory, config);\n    }\n\n    /**\n     * Given a java source directory and configuration, return a CodebaseGraphDTO\n     *\n     * @param srcDirectory The source directory to analyze\n     * @param config The configuration for the graph builder\n     * @return CodebaseGraphDTO\n     * @throws IOException\n     */\n    public CodebaseGraphDTO getCodebaseGraphDTO(String srcDirectory, GraphBuilderConfig config) throws IOException {\n        if (srcDirectory == null || srcDirectory.isEmpty()) {\n            throw new IllegalArgumentException(\"Source directory cannot be null or empty\");\n        }\n        return processWithOpenRewrite(srcDirectory, config);\n    }\n\n    private CodebaseGraphDTO processWithOpenRewrite(String srcDir, GraphBuilderConfig config) throws IOException {\n        File srcDirectory = new File(srcDir);\n\n        JavaParser javaParser = JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        final Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        final Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        final GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        final JavaVisitor<ExecutionContext> javaVisitor = new JavaVisitor<>(dependencyCollector);\n        final JavaVariableTypeVisitor<ExecutionContext> javaVariableTypeVisitor =\n                new JavaVariableTypeVisitor<>(dependencyCollector);\n        final JavaMethodDeclarationVisitor<ExecutionContext> javaMethodDeclarationVisitor =\n                new JavaMethodDeclarationVisitor<>(dependencyCollector);\n\n        try (Stream<Path> pathStream = Files.walk(Paths.get(srcDirectory.getAbsolutePath()))) {\n            List<Path> list;\n            if (config.isExcludeTests()) {\n                list = pathStream\n                        .filter(file -> !file.toString().contains(config.getTestSourceDirectory()))\n                        .collect(Collectors.toList());\n            } else {\n                list = pathStream.collect(Collectors.toList());\n            }\n\n            javaParser\n                    .parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx)\n                    .forEach(cu -> {\n                        javaVisitor.visit(cu, ctx);\n                        javaVariableTypeVisitor.visit(cu, ctx);\n                        javaMethodDeclarationVisitor.visit(cu, ctx);\n                    });\n        }\n\n        removeClassesNotInCodebase(dependencyCollector.getPackagesInCodebase(), classReferencesGraph);\n\n        return new CodebaseGraphDTO(\n                classReferencesGraph, packageReferencesGraph, javaVisitor.getClassToSourceFilePathMapping());\n    }\n\n    // remove node if package not in codebase\n    void removeClassesNotInCodebase(\n            Set<String> packagesInCodebase, Graph<String, DefaultWeightedEdge> classReferencesGraph) {\n\n        // collect nodes to remove\n        Set<String> classesToRemove = new HashSet<>();\n        for (String classFqn : classReferencesGraph.vertexSet()) {\n            if (!packagesInCodebase.contains(getPackage(classFqn))) {\n                classesToRemove.add(classFqn);\n            }\n        }\n\n        classReferencesGraph.removeAllVertices(classesToRemove);\n    }\n\n    String getPackage(String fqn) {\n        // handle no package\n        if (!fqn.contains(\".\")) {\n            return \"\";\n        }\n\n        int lastIndex = fqn.lastIndexOf(\".\");\n        return fqn.substring(0, lastIndex);\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseCodebaseVisitor.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport lombok.Getter;\nimport org.hjug.graphbuilder.DependencyCollector;\nimport org.openrewrite.java.JavaIsoVisitor;\n\n@Getter\npublic abstract class BaseCodebaseVisitor<P> extends JavaIsoVisitor<P> {\n\n    protected final DependencyCollector dependencyCollector;\n\n    protected BaseCodebaseVisitor(DependencyCollector dependencyCollector) {\n        this.dependencyCollector = dependencyCollector;\n    }\n\n    protected abstract String getCurrentOwnerFqn();\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/BaseTypeProcessor.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.graphbuilder.DependencyCollector;\nimport org.openrewrite.Cursor;\nimport org.openrewrite.java.service.AnnotationService;\nimport org.openrewrite.java.tree.Expression;\nimport org.openrewrite.java.tree.J;\nimport org.openrewrite.java.tree.JavaType;\nimport org.openrewrite.java.tree.TypeTree;\n\n@Slf4j\npublic abstract class BaseTypeProcessor {\n\n    private final TypeDependencyExtractor typeDependencyExtractor = new TypeDependencyExtractor();\n\n    protected abstract DependencyCollector getDependencyCollector();\n\n    protected void processType(String ownerFqn, JavaType javaType) {\n        if (javaType == null || javaType instanceof JavaType.Unknown) {\n            return;\n        }\n\n        for (String dependency : typeDependencyExtractor.extractDependencies(javaType)) {\n            getDependencyCollector().addClassDependency(ownerFqn, dependency);\n        }\n    }\n\n    protected void processAnnotation(String ownerFqn, J.Annotation annotation, Cursor cursor) {\n        if (annotation.getType() instanceof JavaType.Unknown) {\n            return;\n        }\n\n        JavaType.Class type = (JavaType.Class) annotation.getType();\n        if (null != type) {\n            String annotationFqn = type.getFullyQualifiedName();\n            log.debug(\"Variable Annotation FQN: {}\", annotationFqn);\n            getDependencyCollector().addClassDependency(ownerFqn, annotationFqn);\n\n            if (null != annotation.getArguments()) {\n                for (Expression argument : annotation.getArguments()) {\n                    processType(ownerFqn, argument.getType());\n                }\n            }\n        }\n    }\n\n    protected void processTypeParameter(String ownerFqn, J.TypeParameter typeParameter, Cursor cursor) {\n        if (null != typeParameter.getBounds()) {\n            for (TypeTree bound : typeParameter.getBounds()) {\n                processType(ownerFqn, bound.getType());\n            }\n        }\n\n        if (!typeParameter.getAnnotations().isEmpty()) {\n            for (J.Annotation annotation : typeParameter.getAnnotations()) {\n                processAnnotation(ownerFqn, annotation, cursor);\n            }\n        }\n    }\n\n    protected void processAnnotations(String ownerFqn, Cursor cursor) {\n        AnnotationService annotationService = new AnnotationService();\n        for (J.Annotation annotation : annotationService.getAllAnnotations(cursor)) {\n            processAnnotation(ownerFqn, annotation, cursor);\n        }\n    }\n\n    protected String getPackageFromFqn(String fqn) {\n        if (!fqn.contains(\".\")) {\n            return \"\";\n        }\n        int lastIndex = fqn.lastIndexOf(\".\");\n        return fqn.substring(0, lastIndex);\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/FqnCapturingProcessor.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport org.openrewrite.java.tree.J;\n\npublic interface FqnCapturingProcessor {\n\n    default J.ClassDeclaration captureClassDeclarations(\n            J.ClassDeclaration classDecl, Map<String, Map<String, String>> fqns) {\n        // get class fqn (including \"$\")\n        String fqn = classDecl.getType().getFullyQualifiedName();\n\n        String currentPackage = getPackage(fqn);\n        String className = getClassName(fqn);\n        Map<String, String> classesInPackage = fqns.getOrDefault(currentPackage, new HashMap<>());\n\n        if (className.contains(\"$\")) {\n            String normalizedClassName = className.replace('$', '.');\n            List<String> parts = Arrays.asList(normalizedClassName.split(\"\\\\.\"));\n            for (int i = 0; i < parts.size(); i++) {\n                String key = String.join(\".\", parts.subList(i, parts.size()));\n                classesInPackage.put(key, currentPackage + \".\" + normalizedClassName);\n            }\n        } else {\n            classesInPackage.put(className, fqn);\n        }\n\n        fqns.put(currentPackage, classesInPackage);\n        return classDecl;\n    }\n\n    default String getPackage(String fqn) {\n        // handle no package\n        if (!fqn.contains(\".\")) {\n            return \"\";\n        }\n\n        int lastIndex = fqn.lastIndexOf(\".\");\n        return fqn.substring(0, lastIndex);\n    }\n\n    /**\n     *\n     * @param fqn\n     * @return Class name (including \"$\") after last period in FQN\n     */\n    default String getClassName(String fqn) {\n        // handle no package\n        if (!fqn.contains(\".\")) {\n            return fqn;\n        }\n\n        int lastIndex = fqn.lastIndexOf(\".\");\n        return fqn.substring(lastIndex + 1);\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitor.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.util.List;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.graphbuilder.DependencyCollector;\nimport org.openrewrite.java.tree.*;\n\n@Slf4j\npublic class JavaClassDeclarationVisitor<P> extends BaseCodebaseVisitor<P> {\n\n    private final BaseTypeProcessor typeProcessor;\n    private String currentOwnerFqn;\n\n    public JavaClassDeclarationVisitor(DependencyCollector dependencyCollector) {\n        super(dependencyCollector);\n        this.typeProcessor = new BaseTypeProcessor() {\n            @Override\n            protected DependencyCollector getDependencyCollector() {\n                return dependencyCollector;\n            }\n        };\n    }\n\n    @Override\n    public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) {\n        JavaType.FullyQualified type = classDecl.getType();\n        if (type == null) {\n            log.warn(\"ClassDeclaration has null type, skipping: {}\", classDecl.getSimpleName());\n            return classDecl;\n        }\n\n        String owningFqn = type.getFullyQualifiedName();\n        String previousOwner = currentOwnerFqn;\n        currentOwnerFqn = owningFqn;\n\n        try {\n            typeProcessor.processType(owningFqn, type);\n\n            TypeTree extendsTypeTree = classDecl.getExtends();\n            if (null != extendsTypeTree) {\n                typeProcessor.processType(owningFqn, extendsTypeTree.getType());\n            }\n\n            List<TypeTree> implementsTypeTree = classDecl.getImplements();\n            if (null != implementsTypeTree) {\n                for (TypeTree typeTree : implementsTypeTree) {\n                    typeProcessor.processType(owningFqn, typeTree.getType());\n                }\n            }\n\n            for (J.Annotation leadingAnnotation : classDecl.getLeadingAnnotations()) {\n                typeProcessor.processAnnotation(owningFqn, leadingAnnotation, getCursor());\n            }\n\n            if (null != classDecl.getTypeParameters()) {\n                for (J.TypeParameter typeParameter : classDecl.getTypeParameters()) {\n                    typeProcessor.processTypeParameter(owningFqn, typeParameter, getCursor());\n                }\n            }\n\n            return super.visitClassDeclaration(classDecl, p);\n        } finally {\n            currentOwnerFqn = previousOwner;\n        }\n    }\n\n    @Override\n    public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, P p) {\n        J.MethodInvocation methodInvocation = super.visitMethodInvocation(method, p);\n        if (currentOwnerFqn == null) {\n            return methodInvocation;\n        }\n\n        JavaType.Method methodType = methodInvocation.getMethodType();\n        if (null != methodType && null != methodType.getDeclaringType()) {\n            typeProcessor.processType(currentOwnerFqn, methodType.getDeclaringType());\n        }\n\n        if (null != methodInvocation.getTypeParameters()\n                && !methodInvocation.getTypeParameters().isEmpty()) {\n            for (Expression typeParameter : methodInvocation.getTypeParameters()) {\n                typeProcessor.processType(currentOwnerFqn, typeParameter.getType());\n            }\n        }\n\n        return methodInvocation;\n    }\n\n    @Override\n    public J.NewClass visitNewClass(J.NewClass newClass, P p) {\n        J.NewClass result = super.visitNewClass(newClass, p);\n        if (currentOwnerFqn != null) {\n            typeProcessor.processType(currentOwnerFqn, newClass.getType());\n        }\n        return result;\n    }\n\n    @Override\n    public J.Lambda visitLambda(J.Lambda lambda, P p) {\n        if (currentOwnerFqn != null && lambda.getType() != null) {\n            typeProcessor.processType(currentOwnerFqn, lambda.getType());\n        }\n\n        // Recursively visit the lambda body to capture method invocations and type references\n        // The super.visitLambda call will traverse into the lambda's body and parameters\n        return super.visitLambda(lambda, p);\n    }\n\n    @Override\n    public J.If visitIf(J.If iff, P p) {\n        return super.visitIf(iff, p);\n    }\n\n    @Override\n    public J.ForLoop visitForLoop(J.ForLoop forLoop, P p) {\n        return super.visitForLoop(forLoop, p);\n    }\n\n    @Override\n    public J.ForEachLoop visitForEachLoop(J.ForEachLoop forEachLoop, P p) {\n        return super.visitForEachLoop(forEachLoop, p);\n    }\n\n    @Override\n    public J.WhileLoop visitWhileLoop(J.WhileLoop whileLoop, P p) {\n        return super.visitWhileLoop(whileLoop, p);\n    }\n\n    @Override\n    public J.DoWhileLoop visitDoWhileLoop(J.DoWhileLoop doWhileLoop, P p) {\n        return super.visitDoWhileLoop(doWhileLoop, p);\n    }\n\n    @Override\n    public J.Switch visitSwitch(J.Switch switchStatement, P p) {\n        return super.visitSwitch(switchStatement, p);\n    }\n\n    @Override\n    public J.Try visitTry(J.Try tryStatement, P p) {\n        J.Try result = super.visitTry(tryStatement, p);\n        if (currentOwnerFqn != null && tryStatement.getCatches() != null) {\n            for (J.Try.Catch catchClause : tryStatement.getCatches()) {\n                if (catchClause.getParameter().getTree() instanceof J.VariableDeclarations) {\n                    J.VariableDeclarations varDecl =\n                            (J.VariableDeclarations) catchClause.getParameter().getTree();\n                    if (varDecl.getTypeExpression() != null) {\n                        typeProcessor.processType(\n                                currentOwnerFqn, varDecl.getTypeExpression().getType());\n                    }\n                }\n            }\n        }\n        return result;\n    }\n\n    @Override\n    public J.InstanceOf visitInstanceOf(J.InstanceOf instanceOf, P p) {\n        J.InstanceOf result = super.visitInstanceOf(instanceOf, p);\n        if (currentOwnerFqn != null && instanceOf.getClazz() != null && instanceOf.getClazz() instanceof TypeTree) {\n            typeProcessor.processType(currentOwnerFqn, ((TypeTree) instanceOf.getClazz()).getType());\n        }\n        return result;\n    }\n\n    @Override\n    public J.TypeCast visitTypeCast(J.TypeCast typeCast, P p) {\n        J.TypeCast result = super.visitTypeCast(typeCast, p);\n        if (currentOwnerFqn != null && typeCast.getClazz() != null) {\n            typeProcessor.processType(\n                    currentOwnerFqn, typeCast.getClazz().getTree().getType());\n        }\n        return result;\n    }\n\n    @Override\n    public J.MemberReference visitMemberReference(J.MemberReference memberRef, P p) {\n        J.MemberReference result = super.visitMemberReference(memberRef, p);\n        if (currentOwnerFqn != null && memberRef.getType() != null) {\n            typeProcessor.processType(currentOwnerFqn, memberRef.getType());\n        }\n        return result;\n    }\n\n    @Override\n    public J.NewArray visitNewArray(J.NewArray newArray, P p) {\n        J.NewArray result = super.visitNewArray(newArray, p);\n        if (currentOwnerFqn != null && newArray.getType() != null) {\n            typeProcessor.processType(currentOwnerFqn, newArray.getType());\n        }\n        return result;\n    }\n\n    @Override\n    protected String getCurrentOwnerFqn() {\n        return currentOwnerFqn;\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitor.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.util.*;\nimport lombok.Getter;\nimport org.openrewrite.java.JavaIsoVisitor;\nimport org.openrewrite.java.tree.J;\n\n/**\n * Captures Fully Qualified Names (FQN) of classes as they will be imported in import statements.\n * fqns map that is populated by this visitor is used to resolve Generic types.\n *\n * @param <P>\n */\n@Getter\npublic class JavaFqnCapturingVisitor<P> extends JavaIsoVisitor<P> {\n\n    // consider using ConcurrentHashMap to scale performance\n    // package -> name, FQN\n    private final Map<String, Map<String, String>> fqnMap = new HashMap<>();\n    private final Set<String> fqns = new HashSet<>();\n\n    @Override\n    public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) {\n        captureClassDeclarations(classDecl, fqnMap);\n        return classDecl;\n    }\n\n    J.ClassDeclaration captureClassDeclarations(J.ClassDeclaration classDecl, Map<String, Map<String, String>> fqnMap) {\n        String fqn = classDecl.getType().getFullyQualifiedName();\n        fqns.add(fqn);\n        return classDecl;\n    }\n\n    String getPackage(String fqn) {\n        // handle no package\n        if (!fqn.contains(\".\")) {\n            return \"\";\n        }\n\n        int lastIndex = fqn.lastIndexOf(\".\");\n        return fqn.substring(0, lastIndex);\n    }\n\n    /**\n     *\n     * @param fqn\n     * @return Class name (including \"$\") after last period in FQN\n     */\n    String getClassName(String fqn) {\n        // handle no package\n        if (!fqn.contains(\".\")) {\n            return fqn;\n        }\n\n        int lastIndex = fqn.lastIndexOf(\".\");\n        return fqn.substring(lastIndex + 1);\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitor.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.util.List;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.graphbuilder.DependencyCollector;\nimport org.openrewrite.java.tree.J;\nimport org.openrewrite.java.tree.JavaType;\nimport org.openrewrite.java.tree.NameTree;\nimport org.openrewrite.java.tree.TypeTree;\n\n@Slf4j\npublic class JavaMethodDeclarationVisitor<P> extends BaseCodebaseVisitor<P> {\n\n    private final BaseTypeProcessor typeProcessor;\n\n    public JavaMethodDeclarationVisitor(DependencyCollector dependencyCollector) {\n        super(dependencyCollector);\n        this.typeProcessor = new BaseTypeProcessor() {\n            @Override\n            protected DependencyCollector getDependencyCollector() {\n                return dependencyCollector;\n            }\n        };\n    }\n\n    @Override\n    public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, P p) {\n        J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(method, p);\n\n        JavaType.Method methodType = methodDeclaration.getMethodType();\n        if (null == methodType) {\n            log.warn(\"MethodDeclaration has null methodType, skipping: {}\", methodDeclaration.getSimpleName());\n            return methodDeclaration;\n        }\n\n        if (methodType.getDeclaringType() == null) {\n            log.warn(\"MethodDeclaration has null declaring type, skipping: {}\", methodDeclaration.getSimpleName());\n            return methodDeclaration;\n        }\n\n        String owner = methodType.getDeclaringType().getFullyQualifiedName();\n\n        TypeTree returnTypeExpression = methodDeclaration.getReturnTypeExpression();\n        if (returnTypeExpression != null) {\n            JavaType returnType = returnTypeExpression.getType();\n\n            if (!(returnType instanceof JavaType.Primitive)) {\n                typeProcessor.processType(owner, returnType);\n            }\n        }\n\n        for (J.Annotation leadingAnnotation : methodDeclaration.getLeadingAnnotations()) {\n            typeProcessor.processAnnotation(owner, leadingAnnotation, getCursor());\n        }\n\n        if (null != methodDeclaration.getTypeParameters()) {\n            for (J.TypeParameter typeParameter : methodDeclaration.getTypeParameters()) {\n                typeProcessor.processTypeParameter(owner, typeParameter, getCursor());\n            }\n        }\n\n        List<NameTree> throwz = methodDeclaration.getThrows();\n        if (null != throwz && !throwz.isEmpty()) {\n            for (NameTree thrown : throwz) {\n                typeProcessor.processType(owner, thrown.getType());\n            }\n        }\n\n        return methodDeclaration;\n    }\n\n    @Override\n    protected String getCurrentOwnerFqn() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitor.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.util.List;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.graphbuilder.DependencyCollector;\nimport org.openrewrite.java.tree.*;\n\n@Slf4j\npublic class JavaVariableTypeVisitor<P> extends BaseCodebaseVisitor<P> {\n\n    private final BaseTypeProcessor typeProcessor;\n\n    public JavaVariableTypeVisitor(DependencyCollector dependencyCollector) {\n        super(dependencyCollector);\n        this.typeProcessor = new BaseTypeProcessor() {\n            @Override\n            protected DependencyCollector getDependencyCollector() {\n                return dependencyCollector;\n            }\n        };\n    }\n\n    @Override\n    public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, P p) {\n        J.VariableDeclarations variableDeclarations = super.visitVariableDeclarations(multiVariable, p);\n\n        List<J.VariableDeclarations.NamedVariable> variables = variableDeclarations.getVariables();\n        if (null == variables || variables.isEmpty() || null == variables.get(0).getVariableType()) {\n            log.debug(\"Skipping variable declaration with null variable type\");\n            return variableDeclarations;\n        }\n\n        JavaType owner = variables.get(0).getVariableType().getOwner();\n        String ownerFqn = \"\";\n\n        if (owner instanceof JavaType.Method) {\n            JavaType.Method m = (JavaType.Method) owner;\n            if (m.getDeclaringType() == null) {\n                log.warn(\"Method owner has null declaring type, skipping variable declaration\");\n                return variableDeclarations;\n            }\n            ownerFqn = m.getDeclaringType().getFullyQualifiedName();\n        } else if (owner instanceof JavaType.Class) {\n            JavaType.Class c = (JavaType.Class) owner;\n            ownerFqn = c.getFullyQualifiedName();\n        } else {\n            log.debug(\"Unknown owner type: {}\", owner != null ? owner.getClass() : \"null\");\n            return variableDeclarations;\n        }\n\n        log.debug(\"Processing variable declaration in: {}\", ownerFqn);\n\n        TypeTree typeTree = variableDeclarations.getTypeExpression();\n\n        JavaType javaType;\n        if (null != typeTree) {\n            javaType = typeTree.getType();\n        } else {\n            return variableDeclarations;\n        }\n\n        typeProcessor.processAnnotations(ownerFqn, getCursor());\n\n        if (javaType instanceof JavaType.Primitive) {\n            return variableDeclarations;\n        }\n\n        typeProcessor.processType(ownerFqn, javaType);\n\n        return variableDeclarations;\n    }\n\n    @Override\n    protected String getCurrentOwnerFqn() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/JavaVisitor.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.util.*;\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.graphbuilder.DependencyCollector;\nimport org.openrewrite.java.tree.*;\n\n@Slf4j\npublic class JavaVisitor<P> extends BaseCodebaseVisitor<P> {\n\n    @Getter\n    private final Map<String, String> classToSourceFilePathMapping = new HashMap<>();\n\n    private final JavaClassDeclarationVisitor<P> javaClassDeclarationVisitor;\n\n    public JavaVisitor(DependencyCollector dependencyCollector) {\n        super(dependencyCollector);\n        javaClassDeclarationVisitor = new JavaClassDeclarationVisitor<>(dependencyCollector);\n    }\n\n    @Override\n    public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P p) {\n        return javaClassDeclarationVisitor.visitClassDeclaration(classDecl, p);\n    }\n\n    // Map each class to its source file\n    @Override\n    public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, P p) {\n        J.CompilationUnit compilationUnit = super.visitCompilationUnit(cu, p);\n\n        J.Package packageDeclaration = compilationUnit.getPackageDeclaration();\n        if (null == packageDeclaration) {\n            return compilationUnit;\n        }\n\n        dependencyCollector.registerPackage(packageDeclaration.getPackageName());\n\n        for (J.ClassDeclaration aClass : compilationUnit.getClasses()) {\n            String classFqn = aClass.getType().getFullyQualifiedName();\n            String sourcePath = compilationUnit.getSourcePath().toUri().toString();\n            classToSourceFilePathMapping.put(classFqn, sourcePath);\n            dependencyCollector.recordClassLocation(classFqn, sourcePath);\n        }\n\n        return compilationUnit;\n    }\n\n    @Override\n    protected String getCurrentOwnerFqn() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/main/java/org/hjug/graphbuilder/visitor/TypeDependencyExtractor.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.openrewrite.java.tree.JavaType;\n\n@Slf4j\npublic class TypeDependencyExtractor {\n\n    /**\n     * Extracts all type dependencies from a JavaType\n     *\n     * @param javaType The type to extract dependencies from\n     * @return Set of fully qualified type names that the given type depends on\n     */\n    public Set<String> extractDependencies(JavaType javaType) {\n        Set<String> dependencies = new HashSet<>();\n        if (javaType == null) {\n            return dependencies;\n        }\n\n        extractDependenciesRecursive(javaType, dependencies);\n        return dependencies;\n    }\n\n    private void extractDependenciesRecursive(JavaType javaType, Set<String> dependencies) {\n        if (javaType instanceof JavaType.Class) {\n            extractFromClass((JavaType.Class) javaType, dependencies);\n        } else if (javaType instanceof JavaType.Parameterized) {\n            extractFromParameterized((JavaType.Parameterized) javaType, dependencies);\n        } else if (javaType instanceof JavaType.GenericTypeVariable) {\n            extractFromGenericTypeVariable((JavaType.GenericTypeVariable) javaType, dependencies);\n        } else if (javaType instanceof JavaType.Array) {\n            extractFromArray((JavaType.Array) javaType, dependencies);\n        }\n    }\n\n    private void extractFromClass(JavaType.Class classType, Set<String> dependencies) {\n        log.debug(\"Class type FQN: {}\", classType.getFullyQualifiedName());\n        dependencies.add(classType.getFullyQualifiedName());\n        extractAnnotations(classType, dependencies);\n    }\n\n    private void extractFromParameterized(JavaType.Parameterized parameterized, Set<String> dependencies) {\n        log.debug(\"Parameterized type FQN: {}\", parameterized.getFullyQualifiedName());\n        dependencies.add(parameterized.getFullyQualifiedName());\n        extractAnnotations(parameterized, dependencies);\n\n        log.debug(\"Nested Parameterized type parameters: {}\", parameterized.getTypeParameters());\n        for (JavaType parameter : parameterized.getTypeParameters()) {\n            extractDependenciesRecursive(parameter, dependencies);\n        }\n    }\n\n    private void extractFromArray(JavaType.Array arrayType, Set<String> dependencies) {\n        log.debug(\"Array Element type: {}\", arrayType.getElemType());\n        extractDependenciesRecursive(arrayType.getElemType(), dependencies);\n    }\n\n    private void extractFromGenericTypeVariable(JavaType.GenericTypeVariable typeVariable, Set<String> dependencies) {\n        log.debug(\"Type parameter type name: {}\", typeVariable.getName());\n\n        for (JavaType bound : typeVariable.getBounds()) {\n            if (bound instanceof JavaType.Class) {\n                dependencies.add(((JavaType.Class) bound).getFullyQualifiedName());\n            } else if (bound instanceof JavaType.Parameterized) {\n                dependencies.add(((JavaType.Parameterized) bound).getFullyQualifiedName());\n            } else {\n                log.debug(\"Unknown type bound: {}\", bound);\n            }\n        }\n    }\n\n    private void extractAnnotations(JavaType.FullyQualified fullyQualified, Set<String> dependencies) {\n        if (!fullyQualified.getAnnotations().isEmpty()) {\n            for (JavaType.FullyQualified annotation : fullyQualified.getAnnotations()) {\n                String annotationFqn = annotation.getFullyQualifiedName();\n                log.debug(\"Annotation FQN: {}\", annotationFqn);\n                dependencies.add(annotationFqn);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/JavaGraphBuilderTest.java",
    "content": "package org.hjug.graphbuilder;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.HashSet;\nimport java.util.Set;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\n\nclass JavaGraphBuilderTest {\n\n    JavaGraphBuilder javaGraphBuilder = new JavaGraphBuilder();\n\n    @DisplayName(\"When source directory input param is empty or null throw IllegalArgumentException.\")\n    @Test\n    void parseSourceDirectoryEmptyTest() {\n        Assertions.assertThrows(\n                IllegalArgumentException.class, () -> javaGraphBuilder.getCodebaseGraphDTO(\"\", false, \"\"));\n        Assertions.assertThrows(\n                IllegalArgumentException.class, () -> javaGraphBuilder.getCodebaseGraphDTO(null, false, \"\"));\n    }\n\n    @DisplayName(\"Given a valid source directory input parameter return a valid graph.\")\n    @Test\n    void parseSourceDirectoryTest() throws IOException {\n        File srcDirectory = new File(\"src/test/resources/javaSrcDirectory\");\n        CodebaseGraphDTO dto = javaGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, \"\");\n        Graph<String, DefaultWeightedEdge> classReferencesGraph = dto.getClassReferencesGraph();\n        assertNotNull(classReferencesGraph);\n        assertEquals(5, classReferencesGraph.vertexSet().size());\n        assertEquals(7, classReferencesGraph.edgeSet().size());\n        assertTrue(classReferencesGraph.containsVertex(\"com.ideacrest.parser.testclasses.A\"));\n        assertTrue(classReferencesGraph.containsVertex(\"com.ideacrest.parser.testclasses.B\"));\n        assertTrue(classReferencesGraph.containsVertex(\"com.ideacrest.parser.testclasses.C\"));\n        assertTrue(classReferencesGraph.containsVertex(\"com.ideacrest.parser.testclasses.D\"));\n        assertTrue(classReferencesGraph.containsVertex(\"com.ideacrest.parser.testclasses.E\"));\n        assertTrue(classReferencesGraph.containsEdge(\n                \"com.ideacrest.parser.testclasses.A\", \"com.ideacrest.parser.testclasses.B\"));\n        assertTrue(classReferencesGraph.containsEdge(\n                \"com.ideacrest.parser.testclasses.B\", \"com.ideacrest.parser.testclasses.C\"));\n        assertTrue(classReferencesGraph.containsEdge(\n                \"com.ideacrest.parser.testclasses.C\", \"com.ideacrest.parser.testclasses.A\"));\n        assertTrue(classReferencesGraph.containsEdge(\n                \"com.ideacrest.parser.testclasses.C\", \"com.ideacrest.parser.testclasses.E\"));\n        assertTrue(classReferencesGraph.containsEdge(\n                \"com.ideacrest.parser.testclasses.D\", \"com.ideacrest.parser.testclasses.A\"));\n        assertTrue(classReferencesGraph.containsEdge(\n                \"com.ideacrest.parser.testclasses.D\", \"com.ideacrest.parser.testclasses.C\"));\n        assertTrue(classReferencesGraph.containsEdge(\n                \"com.ideacrest.parser.testclasses.E\", \"com.ideacrest.parser.testclasses.D\"));\n\n        // confirm edge weight calculations\n        assertEquals(\n                1,\n                getEdgeWeight(\n                        classReferencesGraph,\n                        \"com.ideacrest.parser.testclasses.A\",\n                        \"com.ideacrest.parser.testclasses.B\"));\n        assertEquals(\n                2,\n                getEdgeWeight(\n                        classReferencesGraph,\n                        \"com.ideacrest.parser.testclasses.E\",\n                        \"com.ideacrest.parser.testclasses.D\"));\n    }\n\n    private static double getEdgeWeight(\n            Graph<String, DefaultWeightedEdge> classReferencesGraph, String sourceVertex, String targetVertex) {\n        return classReferencesGraph.getEdgeWeight(classReferencesGraph.getEdge(sourceVertex, targetVertex));\n    }\n\n    @Test\n    void removeClassesNotInCodebase() throws IOException {\n        File srcDirectory = new File(\"src/test/resources/javaSrcDirectory\");\n        CodebaseGraphDTO dto = javaGraphBuilder.getCodebaseGraphDTO(srcDirectory.getAbsolutePath(), false, \"\");\n        Graph<String, DefaultWeightedEdge> classReferencesGraph = dto.getClassReferencesGraph();\n        classReferencesGraph.addVertex(\"org.favioriteoss.FunClass\");\n        classReferencesGraph.addVertex(\"org.favioriteoss.AnotherFunClass\");\n\n        DefaultWeightedEdge edge1 =\n                classReferencesGraph.addEdge(\"com.ideacrest.parser.testclasses.A\", \"org.favioriteoss.FunClass\");\n        DefaultWeightedEdge edge2 =\n                classReferencesGraph.addEdge(\"com.ideacrest.parser.testclasses.A\", \"org.favioriteoss.AnotherFunClass\");\n\n        assertTrue(classReferencesGraph.containsVertex(\"org.favioriteoss.FunClass\"));\n        assertTrue(classReferencesGraph.containsVertex(\"org.favioriteoss.AnotherFunClass\"));\n\n        Set<String> packagesInCodebase = new HashSet<>();\n        packagesInCodebase.add(\"com.ideacrest.parser.testclasses\");\n\n        javaGraphBuilder.removeClassesNotInCodebase(packagesInCodebase, classReferencesGraph);\n\n        assertFalse(classReferencesGraph.containsVertex(\"org.favioriteoss.FunClass\"));\n        assertFalse(classReferencesGraph.containsVertex(\"org.favioriteoss.AnotherFunClass\"));\n        assertFalse(classReferencesGraph.containsEdge(edge1));\n        assertFalse(classReferencesGraph.containsEdge(edge2));\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaClassDeclarationVisitorTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.hjug.graphbuilder.GraphDependencyCollector;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedWeightedGraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\nclass JavaClassDeclarationVisitorTest {\n\n    @Test\n    void visitClasses() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses\");\n\n        org.openrewrite.java.JavaParser javaParser =\n                JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        JavaClassDeclarationVisitor<ExecutionContext> javaVariableCapturingVisitor =\n                new JavaClassDeclarationVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            javaVariableCapturingVisitor.visit(cu, ctx);\n        });\n\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.A\"));\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.B\"));\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.C\"));\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.D\"));\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.MyAnnotation\"));\n        Assertions.assertFalse(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.E\"));\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.F\"));\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.G\"));\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaFqnCapturingVisitorTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\n@Disabled\nclass JavaFqnCapturingVisitorTest {\n\n    @Test\n    void visitClasses() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses\");\n\n        org.openrewrite.java.JavaParser javaParser =\n                JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        JavaFqnCapturingVisitor<ExecutionContext> javaFqnCapturingVisitor = new JavaFqnCapturingVisitor();\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            javaFqnCapturingVisitor.visit(cu, ctx);\n        });\n\n        Map<String, Map<String, String>> fqns = javaFqnCapturingVisitor.getFqnMap();\n        Map<String, String> processed = fqns.get(\"org.hjug.graphbuilder.visitor.testclasses\");\n        Assertions.assertEquals(\"org.hjug.graphbuilder.visitor.testclasses.A\", processed.get(\"A\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass\", processed.get(\"A.InnerClass\"));\n        Assertions.assertEquals(\"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass\", processed.get(\"InnerClass\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner\",\n                processed.get(\"A.InnerClass.InnerInner\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner\",\n                processed.get(\"InnerClass.InnerInner\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner\", processed.get(\"InnerInner\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner\",\n                processed.get(\"A.InnerClass.InnerInner.MegaInner\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner\",\n                processed.get(\"InnerClass.InnerInner.MegaInner\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner\",\n                processed.get(\"InnerInner.MegaInner\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.InnerClass.InnerInner.MegaInner\",\n                processed.get(\"MegaInner\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.StaticInnerClass\", processed.get(\"A.StaticInnerClass\"));\n        Assertions.assertEquals(\n                \"org.hjug.graphbuilder.visitor.testclasses.A.StaticInnerClass\", processed.get(\"StaticInnerClass\"));\n        Assertions.assertEquals(\"org.hjug.graphbuilder.visitor.testclasses.NonPublic\", processed.get(\"NonPublic\"));\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaInitializerBlockVisitorTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.hjug.graphbuilder.GraphDependencyCollector;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedWeightedGraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\nclass JavaInitializerBlockVisitorTest {\n\n    @Test\n    void visitInstanceInitializerBlocks() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers\");\n\n        JavaParser javaParser = JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        JavaClassDeclarationVisitor<ExecutionContext> classDeclarationVisitor =\n                new JavaClassDeclarationVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            classDeclarationVisitor.visit(cu, ctx);\n        });\n\n        // Verify that the test class is in the graph\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\n                        \"org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass\"),\n                \"InitializerBlockTestClass should be in the graph\");\n\n        // Verify ArrayList is captured from instance initializer block: new ArrayList<>()\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"java.util.ArrayList\"),\n                \"ArrayList should be captured from instance initializer block\");\n\n        // Verify edge from InitializerBlockTestClass to ArrayList exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass\",\n                        \"java.util.ArrayList\"),\n                \"Should have edge from InitializerBlockTestClass to ArrayList from initializer block\");\n\n        // Verify HashMap is captured from instance initializer block: new HashMap<>()\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"java.util.HashMap\"),\n                \"HashMap should be captured from instance initializer block\");\n\n        // Verify edge from InitializerBlockTestClass to HashMap exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass\",\n                        \"java.util.HashMap\"),\n                \"Should have edge from InitializerBlockTestClass to HashMap from initializer block\");\n\n        // Verify StringBuilder is captured from instance initializer block: new StringBuilder()\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"java.lang.StringBuilder\"),\n                \"StringBuilder should be captured from instance initializer block\");\n\n        // Verify edge from InitializerBlockTestClass to StringBuilder exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.initializers.InitializerBlockTestClass\",\n                        \"java.lang.StringBuilder\"),\n                \"Should have edge from InitializerBlockTestClass to StringBuilder from initializer block\");\n    }\n\n    @Test\n    void visitStaticInitializerBlocks() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers\");\n\n        JavaParser javaParser = JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        JavaClassDeclarationVisitor<ExecutionContext> classDeclarationVisitor =\n                new JavaClassDeclarationVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            classDeclarationVisitor.visit(cu, ctx);\n        });\n\n        // Verify that the complex test class is in the graph\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\n                        \"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass\"),\n                \"ComplexInitializerClass should be in the graph\");\n\n        // Verify ConcurrentHashMap is captured from static initializer block\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"java.util.concurrent.ConcurrentHashMap\"),\n                \"ConcurrentHashMap should be captured from static initializer block\");\n\n        // Verify edge from ComplexInitializerClass to ConcurrentHashMap exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass\",\n                        \"java.util.concurrent.ConcurrentHashMap\"),\n                \"Should have edge from ComplexInitializerClass to ConcurrentHashMap from static initializer\");\n\n        // Verify AtomicInteger is captured from static initializer block\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"java.util.concurrent.atomic.AtomicInteger\"),\n                \"AtomicInteger should be captured from static initializer block\");\n\n        // Verify edge from ComplexInitializerClass to AtomicInteger exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass\",\n                        \"java.util.concurrent.atomic.AtomicInteger\"),\n                \"Should have edge from ComplexInitializerClass to AtomicInteger from static initializer\");\n\n        // Verify nested classes are captured from instance initializer\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\n                        \"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass$DataProcessor\"),\n                \"DataProcessor nested class should be captured from instance initializer\");\n\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\n                        \"org.hjug.graphbuilder.visitor.testclasses.initializers.ComplexInitializerClass$HelperService\"),\n                \"HelperService nested class should be captured from instance initializer\");\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaLambdaVisitorTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.hjug.graphbuilder.GraphDependencyCollector;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedWeightedGraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\nclass JavaLambdaVisitorTest {\n\n    @Test\n    void visitLambdaBodiesRecursively() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda\");\n\n        JavaParser javaParser = JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        JavaClassDeclarationVisitor<ExecutionContext> classDeclarationVisitor =\n                new JavaClassDeclarationVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            classDeclarationVisitor.visit(cu, ctx);\n        });\n\n        // Verify that the main test class is in the graph\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass\"),\n                \"LambdaTestClass should be in the graph\");\n\n        // Verify that HelperClass is captured as a dependency\n        // This is from field declaration AND from lambda body: helper.process(item)\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass\"),\n                \"HelperClass should be captured from lambda body method invocation\");\n\n        // Verify edge from LambdaTestClass to HelperClass exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass\"),\n                \"Should have edge from LambdaTestClass to HelperClass\");\n\n        // Verify that DataProcessor is captured from lambda body: new DataProcessor()\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor\"),\n                \"DataProcessor should be captured from new class instantiation in lambda body\");\n\n        // Verify edge from LambdaTestClass to DataProcessor exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor\"),\n                \"Should have edge from LambdaTestClass to DataProcessor from lambda body\");\n\n        // Verify that StringBuilder is captured from lambda body: new StringBuilder(s)\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"java.lang.StringBuilder\"),\n                \"StringBuilder should be captured from new class instantiation in lambda body\");\n\n        // Verify edge from LambdaTestClass to StringBuilder exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass\", \"java.lang.StringBuilder\"),\n                \"Should have edge from LambdaTestClass to StringBuilder from lambda body\");\n\n        // Verify that String is captured (from method invocations like s.toUpperCase())\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"java.lang.String\"),\n                \"String should be captured from method invocations in lambda body\");\n\n        // Verify edge weight - multiple lambda usages should increase edge weight\n        DefaultWeightedEdge edge = classReferencesGraph.getEdge(\n                \"org.hjug.graphbuilder.visitor.testclasses.lambda.LambdaTestClass\",\n                \"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor\");\n\n        // DataProcessor is used twice: once in processWithLambda() and once in lambdaWithLocalVariable()\n        Assertions.assertTrue(\n                classReferencesGraph.getEdgeWeight(edge) >= 2.0,\n                \"Edge weight should reflect multiple uses of DataProcessor in lambda bodies\");\n    }\n\n    @Test\n    void visitNestedLambdaBodiesRecursively() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda\");\n\n        JavaParser javaParser = JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        JavaClassDeclarationVisitor<ExecutionContext> classDeclarationVisitor =\n                new JavaClassDeclarationVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            classDeclarationVisitor.visit(cu, ctx);\n        });\n\n        // Verify that the nested lambda test class is in the graph\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass\"),\n                \"NestedLambdaTestClass should be in the graph\");\n\n        // Verify DataProcessor is captured from INNER lambda: new DataProcessor() inside nested lambda\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor\"),\n                \"DataProcessor should be captured from inner nested lambda body\");\n\n        // Verify edge from NestedLambdaTestClass to DataProcessor exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor\"),\n                \"Should have edge from NestedLambdaTestClass to DataProcessor from nested lambda\");\n\n        // Verify HelperClass is captured from nested lambda method invocation\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass\"),\n                \"HelperClass should be captured from nested lambda method invocation\");\n\n        // Verify edge from NestedLambdaTestClass to HelperClass exists\n        Assertions.assertTrue(\n                classReferencesGraph.containsEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass\"),\n                \"Should have edge from NestedLambdaTestClass to HelperClass from nested lambda\");\n\n        // Verify edge weight reflects multiple nested lambda usages\n        DefaultWeightedEdge dataProcessorEdge = classReferencesGraph.getEdge(\n                \"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass\",\n                \"org.hjug.graphbuilder.visitor.testclasses.lambda.DataProcessor\");\n\n        // DataProcessor is used in multiple nested lambdas: processNestedLambdas() and deeplyNestedLambdaWithNewClass()\n        Assertions.assertTrue(\n                classReferencesGraph.getEdgeWeight(dataProcessorEdge) >= 2.0,\n                \"Edge weight should reflect multiple uses of DataProcessor in nested lambda bodies\");\n\n        // Verify that deeply nested instantiations are captured\n        DefaultWeightedEdge helperEdge = classReferencesGraph.getEdge(\n                \"org.hjug.graphbuilder.visitor.testclasses.lambda.NestedLambdaTestClass\",\n                \"org.hjug.graphbuilder.visitor.testclasses.lambda.HelperClass\");\n\n        // HelperClass is used in field declaration and in nested lambdas\n        Assertions.assertTrue(\n                classReferencesGraph.getEdgeWeight(helperEdge) >= 2.0,\n                \"Edge weight should reflect HelperClass usage in nested lambda blocks\");\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodDeclarationVisitorTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.hjug.graphbuilder.GraphDependencyCollector;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedWeightedGraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\nclass JavaMethodDeclarationVisitorTest {\n\n    @Test\n    void visitMethodDeclarations() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses\");\n\n        org.openrewrite.java.JavaParser javaParser =\n                JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        JavaMethodDeclarationVisitor<ExecutionContext> methodDeclarationVisitor =\n                new JavaMethodDeclarationVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            methodDeclarationVisitor.visit(cu, ctx);\n        });\n\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.A\"));\n\n        // TODO: Assert stuff\n        /*   Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex(\"org.hjug.javaVariableVisitorTestClasses.A\"));\n        Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex(\"org.hjug.javaVariableVisitorTestClasses.B\"));\n        Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex(\"org.hjug.javaVariableVisitorTestClasses.C\"));\n        Assertions.assertFalse(methodDeclarationVisitor.getClassReferencesGraph().containsVertex(\"org.hjug.javaVariableVisitorTestClasses.D\"));\n        Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex(\"org.hjug.javaVariableVisitorTestClasses.MyAnnotation\"));\n        Assertions.assertFalse(methodDeclarationVisitor.getClassReferencesGraph().containsVertex(\"org.hjug.javaVariableVisitorTestClasses.E\"));\n        Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex(\"org.hjug.javaVariableVisitorTestClasses.F\"));\n        Assertions.assertTrue(methodDeclarationVisitor.getClassReferencesGraph().containsVertex(\"org.hjug.javaVariableVisitorTestClasses.G\"));*/\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaMethodInvocationVisitorTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.hjug.graphbuilder.GraphDependencyCollector;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.graph.SimpleDirectedWeightedGraph;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\nclass JavaMethodInvocationVisitorTest {\n\n    @Test\n    void visitMethodInvocations() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation\");\n\n        JavaParser javaParser = JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        JavaClassDeclarationVisitor<ExecutionContext> classDeclarationVisitor =\n                new JavaClassDeclarationVisitor<>(dependencyCollector);\n        JavaVariableTypeVisitor<ExecutionContext> variableTypeVisitor =\n                new JavaVariableTypeVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            classDeclarationVisitor.visit(cu, ctx);\n            variableTypeVisitor.visit(cu, ctx);\n        });\n\n        Graph<String, DefaultWeightedEdge> graph = classReferencesGraph;\n        Assertions.assertTrue(graph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A\"));\n        Assertions.assertTrue(graph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B\"));\n        Assertions.assertTrue(graph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C\"));\n\n        Assertions.assertEquals(\n                3,\n                graph.getEdgeWeight(graph.getEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.B\")));\n        Assertions.assertEquals(\n                3,\n                graph.getEdgeWeight(graph.getEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.A\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.methodInvocation.C\")));\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorFullTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.hjug.graphbuilder.GraphDependencyCollector;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.graph.SimpleDirectedWeightedGraph;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\npublic class JavaNewClassVisitorFullTest {\n\n    @Test\n    void visitNewClass() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass\");\n\n        JavaParser javaParser = JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        final JavaVisitor<ExecutionContext> javaVisitor = new JavaVisitor<>(dependencyCollector);\n        final JavaVariableTypeVisitor<ExecutionContext> javaVariableTypeVisitor =\n                new JavaVariableTypeVisitor<>(dependencyCollector);\n        final JavaMethodDeclarationVisitor<ExecutionContext> javaMethodDeclarationVisitor =\n                new JavaMethodDeclarationVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            javaVisitor.visit(cu, ctx);\n            javaVariableTypeVisitor.visit(cu, ctx);\n            javaMethodDeclarationVisitor.visit(cu, ctx);\n        });\n\n        Graph<String, DefaultWeightedEdge> graph = classReferencesGraph;\n        Assertions.assertTrue(graph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.newClass.A\"));\n        Assertions.assertTrue(graph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.newClass.B\"));\n        Assertions.assertTrue(graph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.newClass.C\"));\n\n        // capturing counts of all types\n        Assertions.assertEquals(\n                6,\n                graph.getEdgeWeight(graph.getEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.newClass.A\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.newClass.B\")));\n\n        Assertions.assertEquals(\n                3,\n                graph.getEdgeWeight(graph.getEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.newClass.A\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.newClass.C\")));\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaNewClassVisitorTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.hjug.graphbuilder.GraphDependencyCollector;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.graph.SimpleDirectedWeightedGraph;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\npublic class JavaNewClassVisitorTest {\n\n    @Test\n    void visitNewClass() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass\");\n\n        JavaParser javaParser = JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        JavaClassDeclarationVisitor<ExecutionContext> classDeclarationVisitor =\n                new JavaClassDeclarationVisitor<>(dependencyCollector);\n        JavaVariableTypeVisitor<ExecutionContext> variableTypeVisitor =\n                new JavaVariableTypeVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            classDeclarationVisitor.visit(cu, ctx);\n            variableTypeVisitor.visit(cu, ctx);\n        });\n\n        Graph<String, DefaultWeightedEdge> graph = classReferencesGraph;\n        Assertions.assertTrue(graph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.newClass.A\"));\n        Assertions.assertTrue(graph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.newClass.B\"));\n        Assertions.assertTrue(graph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.newClass.C\"));\n\n        // only looking for what was visited by classDeclarationVisitor and variableTypeVisitor\n        Assertions.assertEquals(\n                5,\n                graph.getEdgeWeight(graph.getEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.newClass.A\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.newClass.B\")));\n\n        Assertions.assertEquals(\n                3,\n                graph.getEdgeWeight(graph.getEdge(\n                        \"org.hjug.graphbuilder.visitor.testclasses.newClass.A\",\n                        \"org.hjug.graphbuilder.visitor.testclasses.newClass.C\")));\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVariableTypeVisitorTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.hjug.graphbuilder.GraphDependencyCollector;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedWeightedGraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\nclass JavaVariableTypeVisitorTest {\n\n    @Test\n    void visitClasses() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses\");\n\n        org.openrewrite.java.JavaParser javaParser =\n                JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        JavaVariableTypeVisitor<ExecutionContext> javaVariableCapturingVisitor =\n                new JavaVariableTypeVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            javaVariableCapturingVisitor.visit(cu, ctx);\n        });\n\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.A\"));\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.B\"));\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.C\"));\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.D\"));\n        Assertions.assertTrue(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.E\"));\n        Assertions.assertTrue(\n                classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.MyAnnotation\"));\n        Assertions.assertFalse(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.F\"));\n        Assertions.assertFalse(classReferencesGraph.containsVertex(\"org.hjug.graphbuilder.visitor.testclasses.G\"));\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/JavaVisitorTest.java",
    "content": "package org.hjug.graphbuilder.visitor;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport org.hjug.graphbuilder.GraphDependencyCollector;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.graph.SimpleDirectedWeightedGraph;\nimport org.junit.jupiter.api.Test;\nimport org.openrewrite.ExecutionContext;\nimport org.openrewrite.InMemoryExecutionContext;\nimport org.openrewrite.java.JavaParser;\n\nclass JavaVisitorTest {\n\n    @Test\n    void visitClasses() throws IOException {\n\n        File srcDirectory = new File(\"src/test/java/org/hjug/graphbuilder/visitor/testclasses\");\n\n        org.openrewrite.java.JavaParser javaParser =\n                JavaParser.fromJavaVersion().build();\n        ExecutionContext ctx = new InMemoryExecutionContext(Throwable::printStackTrace);\n\n        final Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        final Graph<String, DefaultWeightedEdge> packageReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        final GraphDependencyCollector dependencyCollector =\n                new GraphDependencyCollector(classReferencesGraph, packageReferencesGraph);\n\n        final JavaVisitor<ExecutionContext> javaVisitor = new JavaVisitor<>(dependencyCollector);\n\n        List<Path> list = Files.walk(Paths.get(srcDirectory.getAbsolutePath())).collect(Collectors.toList());\n        javaParser.parse(list, Paths.get(srcDirectory.getAbsolutePath()), ctx).forEach(cu -> {\n            System.out.println(cu.getSourcePath());\n            javaVisitor.visit(cu, ctx);\n        });\n\n        assertEquals(5, dependencyCollector.getPackagesInCodebase().size());\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/A.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\nimport java.util.List;\nimport java.util.Map;\n\n@MyAnnotation\npublic class A<T> {\n\n    //\tpublic A(B cB, C cC){}\n\n    B<? extends C> crazyType;\n\n    @MyAnnotation\n    @MyOtherAnnotation\n    int intVar, intVar2;\n\n    @MyAnnotation\n    @MyOtherAnnotation\n    C rawC;\n\n    B<B> b, b3;\n    C<B.InnerB, B> c;\n    D[] ds;\n    D d;\n\n    @MyAnnotation\n    B<C>[] arrayOfGenericBsWithCTypeParam;\n\n    @MyAnnotation\n    B<C[]> bWithArrayOfCs;\n\n    List<A<B>> listWithNestedGenric;\n    Map<A, B> map;\n    List<? extends List<? extends Number>> listOfListsOfNumbers;\n\n    @MyAnnotation\n    <T extends A, U extends B> F doSomething(B<C> paramB, C<B.InnerB, B> genericParam) {\n        List<B<E>> list3;\n        A<E> a2;\n        B<C> b2;\n        C<A, B> c2;\n\n        H h = new H();\n\n        B.<H>invocationTest(h);\n\n        return new G();\n    }\n\n    class InnerClass {\n        class InnerInner {\n            class MegaInner {\n                D d;\n            }\n        }\n    }\n\n    static class StaticInnerClass {}\n}\n\nclass NonPublic {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/B.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\npublic class B<T> {\n\n    static <T extends B> D invocationTest(T type) {\n        return new D();\n    }\n\n    static class InnerB extends A {}\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/C.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\npublic class C<T extends A, U extends B> {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/D.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\npublic class D {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/E.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\npublic interface E {\n    void foo(A a);\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/F.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\npublic class F {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/G.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\npublic class G extends F {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/H.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\npublic class H<T> extends B<T> {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyAnnotation.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Target;\n\n@Target(ElementType.TYPE_USE)\n@interface MyAnnotation {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/MyOtherAnnotation.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Target;\n\n@Target(ElementType.TYPE_USE)\n@interface MyOtherAnnotation {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/ComplexInitializerClass.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.initializers;\n\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class ComplexInitializerClass {\n\n    private static ConcurrentHashMap<String, String> staticCache;\n    private static AtomicInteger instanceCounter;\n\n    private DataProcessor processor;\n    private HelperService helper;\n\n    // Static initializer with new class instantiations\n    static {\n        staticCache = new ConcurrentHashMap<>();\n        instanceCounter = new AtomicInteger(0);\n        staticCache.put(\"initialized\", \"true\");\n    }\n\n    // Instance initializer with dependencies\n    {\n        processor = new DataProcessor();\n        helper = new HelperService();\n        instanceCounter.incrementAndGet();\n    }\n\n    // Another static initializer\n    static {\n        staticCache.put(\"version\", \"1.0\");\n    }\n\n    public void process() {\n        processor.execute();\n    }\n\n    static class DataProcessor {\n        public void execute() {}\n    }\n\n    static class HelperService {}\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/initializers/InitializerBlockTestClass.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.initializers;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class InitializerBlockTestClass {\n\n    private List<String> items;\n    private Map<String, Integer> counters;\n    private StringBuilder builder;\n\n    // Instance initializer block\n    {\n        items = new ArrayList<>();\n        counters = new HashMap<>();\n        builder = new StringBuilder(\"Initialized\");\n    }\n\n    // Static initializer block\n    static {\n        System.out.println(\"Static initializer\");\n    }\n\n    // Another instance initializer block with method invocations\n    {\n        items.add(\"default\");\n        counters.put(\"default\", 0);\n        builder.append(\" with defaults\");\n    }\n\n    public InitializerBlockTestClass() {\n        // Constructor\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/DataProcessor.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.lambda;\n\npublic class DataProcessor {\n\n    public String transform(String data) {\n        return data;\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/HelperClass.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.lambda;\n\npublic class HelperClass {\n\n    public String process(String input) {\n        return input.toUpperCase();\n    }\n\n    public static String staticProcess(String input) {\n        return input.toLowerCase();\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/LambdaTestClass.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.lambda;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class LambdaTestClass {\n\n    private List<String> items = new ArrayList<>();\n    private HelperClass helper = new HelperClass();\n\n    public void processWithLambda() {\n        // Lambda with method invocation on helper class\n        items.forEach(item -> helper.process(item));\n\n        // Lambda with multiple method invocations\n        items.stream().map(s -> s.toUpperCase()).filter(s -> s.length() > 5).collect(Collectors.toList());\n\n        // Lambda with new class instantiation - creates dependency on DataProcessor\n        items.stream().map(s -> new DataProcessor().transform(s)).collect(Collectors.toList());\n\n        // Lambda with new StringBuilder instantiation\n        items.stream().map(s -> new StringBuilder(s)).collect(Collectors.toList());\n\n        // Nested lambda\n        items.stream()\n                .map(s -> s.chars().mapToObj(c -> String.valueOf((char) c)).collect(Collectors.joining()))\n                .collect(Collectors.toList());\n\n        // Lambda with static method reference\n        items.stream().map(HelperClass::staticProcess).forEach(System.out::println);\n\n        // Lambda with type cast\n        items.stream().map(s -> (CharSequence) s).collect(Collectors.toList());\n    }\n\n    public void lambdaWithLocalVariable() {\n        items.forEach(item -> {\n            DataProcessor processor = new DataProcessor();\n            String processed = processor.transform(item);\n            System.out.println(processed);\n        });\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/lambda/NestedLambdaTestClass.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.lambda;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class NestedLambdaTestClass {\n\n    private List<List<String>> nestedItems = new ArrayList<>();\n    private HelperClass helper = new HelperClass();\n\n    public void processNestedLambdas() {\n        // Nested lambda with DataProcessor instantiation in inner lambda\n        nestedItems.stream()\n                .map(innerList -> innerList.stream()\n                        .map(s -> new DataProcessor().transform(s))\n                        .collect(Collectors.toList()))\n                .collect(Collectors.toList());\n\n        // Nested lambda with HelperClass method invocation in inner lambda\n        nestedItems.stream()\n                .flatMap(innerList -> innerList.stream().map(s -> helper.process(s)))\n                .collect(Collectors.toList());\n\n        // Triple nested lambda with multiple dependencies\n        nestedItems.stream()\n                .map(outerList -> outerList.stream()\n                        .map(middleItem -> middleItem\n                                .chars()\n                                .mapToObj(c -> new DataProcessor().transform(String.valueOf((char) c)))\n                                .collect(Collectors.joining()))\n                        .collect(Collectors.toList()))\n                .collect(Collectors.toList());\n    }\n\n    public void deeplyNestedLambdaWithNewClass() {\n        // Deeply nested lambda creating new instances at each level\n        nestedItems.stream()\n                .map(level1 -> {\n                    DataProcessor processor1 = new DataProcessor();\n                    return level1.stream()\n                            .map(level2 -> {\n                                HelperClass helper2 = new HelperClass();\n                                return helper2.process(processor1.transform(level2));\n                            })\n                            .collect(Collectors.toList());\n                })\n                .collect(Collectors.toList());\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/A.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.methodInvocation;\n\npublic class A {\n\n    A doSomething() {\n        B.<C>invocationTest(new D());\n        A a = B.<C>invocationTest(new D());\n        // TODO: add visitor for J.ReturnType\n        return B.<C>invocationTest(new D());\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/B.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.methodInvocation;\n\npublic class B<T> {\n\n    static <T extends B> A invocationTest(T type) {\n        return new A();\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/C.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.methodInvocation;\n\npublic class C<T> extends B<T> {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/methodInvocation/D.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.methodInvocation;\n\npublic class D<T> extends C<T> {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/A.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.newClass;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class A {\n\n    B newClassMethod() {\n        new C();\n        C c = new C();\n\n        // var treated like \"B\", counts as 2\n        var b = new B(null);\n        // <> treated like <B>, counts as 2\n        List<B> listB = new ArrayList<>();\n\n        // TODO: add visitor for J.ReturnType\n        return new B(c);\n    }\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/B.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.newClass;\n\npublic class B {\n\n    public B(C c) {}\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/java/org/hjug/graphbuilder/visitor/testclasses/newClass/C.java",
    "content": "package org.hjug.graphbuilder.visitor.testclasses.newClass;\n\npublic class C {}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/A.java",
    "content": "package com.ideacrest.parser.testclasses;\n\npublic class A {\n\tB b;\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/B.java",
    "content": "package com.ideacrest.parser.testclasses;\n\npublic class B {\n\tC c;\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/C.java",
    "content": "package com.ideacrest.parser.testclasses;\n\npublic class C {\n\tA a;\n\tE e;\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/D.java",
    "content": "package com.ideacrest.parser.testclasses;\n\npublic class D {\n\tA a;\n\tC c;\n}\n"
  },
  {
    "path": "codebase-graph-builder/src/test/resources/javaSrcDirectory/com/ideacrest/parser/testclasses/E.java",
    "content": "package com.ideacrest.parser.testclasses;\n\npublic class E {\n\tD d;\n\tD d2;\n}\n"
  },
  {
    "path": "cost-benefit-calculator/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.costbenefitcalculator</groupId>\n    <artifactId>cost-benefit-calculator</artifactId>\n\n    <name>RefactorFirst Cost Benefit Calculator</name>\n\n    <dependencies>\n        <!-- Needed for PMD -->\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.codebasegraphbuilder</groupId>\n            <artifactId>codebase-graph-builder</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.changepronenessranker</groupId>\n            <artifactId>change-proneness-ranker</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.effortranker</groupId>\n            <artifactId>effort-ranker</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.dsm</groupId>\n            <artifactId>graph-algorithms</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.testresources</groupId>\n            <artifactId>test-resources</artifactId>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "cost-benefit-calculator/src/main/java/org/hjug/cbc/CostBenefitCalculator.java",
    "content": "package org.hjug.cbc;\n\nimport static net.sourceforge.pmd.RuleViolation.CLASS_NAME;\nimport static net.sourceforge.pmd.RuleViolation.PACKAGE_NAME;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport lombok.extern.slf4j.Slf4j;\nimport net.sourceforge.pmd.*;\nimport net.sourceforge.pmd.lang.LanguageRegistry;\nimport org.eclipse.jgit.api.errors.GitAPIException;\nimport org.hjug.git.ChangePronenessRanker;\nimport org.hjug.git.GitLogReader;\nimport org.hjug.git.ScmLogInfo;\nimport org.hjug.metrics.*;\nimport org.hjug.metrics.rules.CBORule;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n@Slf4j\npublic class CostBenefitCalculator implements AutoCloseable {\n\n    private Report report;\n    private final String repositoryPath;\n    private GitLogReader gitLogReader;\n\n    private final ChangePronenessRanker changePronenessRanker;\n    private final Map<String, String> classToSourceFilePathMapping;\n\n    public CostBenefitCalculator(String repositoryPath, Map<String, String> classToSourceFilePathMapping) {\n        this.repositoryPath = repositoryPath;\n\n        log.info(\"Initiating Cost Benefit calculation\");\n        try {\n            gitLogReader = new GitLogReader(new File(repositoryPath));\n        } catch (IOException e) {\n            log.error(\"Failure to access Git repository\", e);\n        }\n\n        changePronenessRanker = new ChangePronenessRanker(gitLogReader);\n        this.classToSourceFilePathMapping = classToSourceFilePathMapping;\n    }\n\n    @Override\n    public void close() throws Exception {\n        gitLogReader.close();\n    }\n\n    // copied from PMD's PmdTaskImpl.java and modified\n    public void runPmdAnalysis() throws IOException {\n        PMDConfiguration configuration = new PMDConfiguration();\n\n        try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) {\n            loadRules(pmd);\n\n            try (Stream<Path> files = Files.walk(Paths.get(repositoryPath))) {\n                files.filter(Files::isRegularFile).forEach(file -> pmd.files().addFile(file));\n            }\n\n            report = pmd.performAnalysisAndCollectReport();\n        }\n    }\n\n    public void runPmdAnalysis(boolean excludeTests, String testSourceDirectory) throws IOException {\n        PMDConfiguration configuration = new PMDConfiguration();\n\n        try (PmdAnalysis pmd = PmdAnalysis.create(configuration)) {\n            loadRules(pmd);\n\n            try (Stream<Path> files = Files.walk(Paths.get(repositoryPath))) {\n                Stream<Path> pathStream;\n                if (excludeTests) {\n                    pathStream = files.filter(Files::isRegularFile)\n                            .filter(file -> !file.toString().contains(testSourceDirectory));\n                } else {\n                    pathStream = files.filter(Files::isRegularFile);\n                }\n\n                pathStream.forEach(file -> pmd.files().addFile(file));\n            }\n\n            report = pmd.performAnalysisAndCollectReport();\n        }\n    }\n\n    private void loadRules(PmdAnalysis pmd) {\n        RuleSetLoader rulesetLoader = pmd.newRuleSetLoader();\n        pmd.addRuleSets(rulesetLoader.loadRuleSetsWithoutException(List.of(\"category/java/design.xml\")));\n\n        Rule cboClassRule = new CBORule();\n        cboClassRule.setLanguage(LanguageRegistry.PMD.getLanguageByFullName(\"Java\"));\n        pmd.addRuleSet(RuleSet.forSingleRule(cboClassRule));\n\n        log.info(\"files to be scanned: \" + Paths.get(repositoryPath));\n    }\n\n    public List<RankedDisharmony> calculateGodClassCostBenefitValues() {\n        List<GodClass> godClasses = getGodClasses();\n\n        List<ScmLogInfo> scmLogInfos = getRankedChangeProneness(godClasses);\n\n        Map<String, ScmLogInfo> rankedLogInfosByPath = getRankedLogInfosByPath(scmLogInfos);\n\n        List<RankedDisharmony> rankedDisharmonies = godClasses.stream()\n                .filter(godClass -> rankedLogInfosByPath.containsKey(godClass.getFileName()))\n                .map(godClass -> new RankedDisharmony(godClass, rankedLogInfosByPath.get(godClass.getFileName())))\n                .sorted(Comparator.comparing(RankedDisharmony::getRawPriority).reversed())\n                .collect(Collectors.toList());\n\n        int godClassPriority = 1;\n        for (RankedDisharmony rankedGodClassDisharmony : rankedDisharmonies) {\n            rankedGodClassDisharmony.setPriority(godClassPriority++);\n        }\n\n        return rankedDisharmonies;\n    }\n\n    private static Map<String, ScmLogInfo> getRankedLogInfosByPath(List<ScmLogInfo> scmLogInfos) {\n        return scmLogInfos.stream().collect(Collectors.toMap(ScmLogInfo::getPath, logInfo -> logInfo, (a, b) -> b));\n    }\n\n    private static Map<String, ScmLogInfo> getRankedLogInfosByClass(List<ScmLogInfo> scmLogInfos) {\n        return scmLogInfos.stream()\n                .collect(Collectors.toMap(ScmLogInfo::getClassName, logInfo -> logInfo, (a, b) -> b));\n    }\n\n    private List<GodClass> getGodClasses() {\n        List<GodClass> godClasses = new ArrayList<>();\n        for (RuleViolation violation : report.getViolations()) {\n            if (violation.getRule().getName().contains(\"GodClass\")) {\n                GodClass godClass = new GodClass(\n                        violation.getAdditionalInfo().get(CLASS_NAME),\n                        getFileName(violation),\n                        violation.getAdditionalInfo().get(PACKAGE_NAME),\n                        violation.getDescription());\n                log.info(\"God Class identified: {}\", godClass.getFileName());\n                godClasses.add(godClass);\n            }\n        }\n\n        GodClassRanker godClassRanker = new GodClassRanker();\n        godClassRanker.rankGodClasses(godClasses);\n\n        return godClasses;\n    }\n\n    public <T extends Disharmony> List<ScmLogInfo> getRankedChangeProneness(List<T> disharmonies) {\n        log.info(\"Calculating Change Proneness\");\n\n        Map<String, String> innerClassPaths = new ConcurrentHashMap<>();\n        Map<String, ScmLogInfo> scmLogInfosByPath = new ConcurrentHashMap<>();\n\n        List<Optional<ScmLogInfo>> scmLogInfos = disharmonies.parallelStream()\n                .map(disharmony -> {\n                    String className = disharmony.getClassName();\n                    String path = null;\n                    ScmLogInfo scmLogInfo = null;\n                    try {\n                        if (className.contains(\"$\")\n                                && classToSourceFilePathMapping.containsKey(\n                                        className.substring(0, className.indexOf(\"$\")))) {\n                            path = classToSourceFilePathMapping.get(className.substring(0, className.indexOf(\"$\")));\n                            log.debug(\"Found source file {} for nested class: {}\", path, className);\n                            innerClassPaths.put(className, path);\n                        } else {\n                            path = disharmony.getFileName();\n                            try {\n                                log.debug(\"Reading scmLogInfo for {}\", path);\n                                scmLogInfo = gitLogReader.fileLog(path);\n                                scmLogInfo.setClassName(className);\n                                log.debug(\"Successfully fetched scmLogInfo for {}\", scmLogInfo.getPath());\n                                scmLogInfosByPath.put(path, scmLogInfo);\n                            } catch (GitAPIException | IOException e) {\n                                log.error(\"Error reading Git repository contents.\", e);\n                            }\n                        }\n                    } catch (NullPointerException e) {\n                        // Should not be reached\n                        log.error(\n                                \"Error looking up class SCM info.  If this error is encountered, \"\n                                        + \"please log a bug on the RefactorFirst project and describe if the class is a nested class, lambda, etc. \\nClass: {}, Path: {}\",\n                                className,\n                                path,\n                                e);\n                    }\n\n                    Optional<ScmLogInfo> scmLogInfoOptional = Optional.ofNullable(scmLogInfo);\n                    if (scmLogInfoOptional.isEmpty()) {\n                        log.warn(\"No scmLogInfo found for class: {} at path: {}\", className, path);\n                    }\n                    return scmLogInfoOptional;\n                })\n                .collect(Collectors.toList());\n\n        List<Optional<ScmLogInfo>> innerClassScmLogInfos = innerClassPaths.entrySet().parallelStream()\n                .map(innerClassPathEntry -> {\n                    ScmLogInfo scmLogInfo = scmLogInfosByPath.get(innerClassPathEntry.getValue());\n\n                    ScmLogInfo innerClassScmLogInfo = null;\n                    if (scmLogInfo == null) {\n                        String className = innerClassPathEntry.getKey();\n                        String path = classToSourceFilePathMapping.get(className.substring(0, className.indexOf(\"$\")));\n                        log.debug(\"Reading scmLogInfo for inner class {}\", canonicaliseURIStringForRepoLookup(path));\n                        try {\n                            innerClassScmLogInfo = gitLogReader.fileLog(canonicaliseURIStringForRepoLookup(path));\n                            innerClassScmLogInfo.setClassName(className);\n                            log.debug(\n                                    \"Successfully fetched scmLogInfo for inner class {} at {}\",\n                                    innerClassScmLogInfo.getClassName(),\n                                    innerClassScmLogInfo.getPath());\n                            scmLogInfosByPath.put(path, innerClassScmLogInfo);\n                        } catch (GitAPIException | IOException e) {\n                            log.error(\n                                    \"Error reading Git repository contents for class {} with file path {}\",\n                                    className,\n                                    path,\n                                    e);\n                        }\n                    } else {\n                        innerClassScmLogInfo = new ScmLogInfo(\n                                innerClassPathEntry.getValue(),\n                                innerClassPathEntry.getKey(),\n                                scmLogInfo.getEarliestCommit(),\n                                scmLogInfo.getMostRecentCommit(),\n                                scmLogInfo.getCommitCount());\n\n                        String className = innerClassPathEntry.getKey();\n                        innerClassScmLogInfo.setClassName(className);\n                        String path = classToSourceFilePathMapping.get(className.substring(0, className.indexOf(\"$\")));\n                        scmLogInfosByPath.put(path, innerClassScmLogInfo);\n                    }\n                    return Optional.ofNullable(innerClassScmLogInfo);\n                })\n                .collect(Collectors.toList());\n\n        scmLogInfos.addAll(innerClassScmLogInfos);\n\n        List<ScmLogInfo> sortedScmInfos = new ArrayList<>(scmLogInfos.stream()\n                .filter(Optional::isPresent)\n                .map(Optional::get)\n                .collect(Collectors.toList()));\n\n        changePronenessRanker.rankChangeProneness(sortedScmInfos);\n        return sortedScmInfos;\n    }\n\n    public List<RankedDisharmony> calculateCBOCostBenefitValues() {\n        List<CBOClass> cboClasses = getCBOClasses();\n\n        List<ScmLogInfo> scmLogInfos = getRankedChangeProneness(cboClasses);\n\n        Map<String, ScmLogInfo> rankedLogInfosByPath = getRankedLogInfosByPath(scmLogInfos);\n        for (Map.Entry<String, ScmLogInfo> stringScmLogInfoEntry : rankedLogInfosByPath.entrySet()) {\n            log.debug(\n                    \"ScmLogInfo entry: {} path: {}\",\n                    stringScmLogInfoEntry.getKey(),\n                    stringScmLogInfoEntry.getValue().getPath());\n        }\n\n        List<RankedDisharmony> rankedDisharmonies = new ArrayList<>();\n        for (CBOClass cboClass : cboClasses) {\n            log.debug(\"CBO Class identified: {}\", cboClass.getFileName());\n            log.debug(\n                    \"ScmLogInfo: {}\",\n                    rankedLogInfosByPath.get(cboClass.getFileName()).getPath());\n            rankedDisharmonies.add(new RankedDisharmony(cboClass, rankedLogInfosByPath.get(cboClass.getFileName())));\n        }\n\n        rankedDisharmonies.sort(\n                Comparator.comparing(RankedDisharmony::getRawPriority).reversed());\n\n        int cboPriority = 1;\n        for (RankedDisharmony rankedCBODisharmony : rankedDisharmonies) {\n            rankedCBODisharmony.setPriority(cboPriority++);\n        }\n\n        return rankedDisharmonies;\n    }\n\n    private List<CBOClass> getCBOClasses() {\n        List<CBOClass> cboClasses = new ArrayList<>();\n        for (RuleViolation violation : report.getViolations()) {\n            if (violation.getRule().getName().contains(\"CBORule\")) {\n                log.info(violation.getDescription());\n                CBOClass godClass = new CBOClass(\n                        violation.getAdditionalInfo().get(CLASS_NAME),\n                        getFileName(violation),\n                        violation.getAdditionalInfo().get(PACKAGE_NAME),\n                        violation.getDescription());\n                log.debug(\"Highly Coupled class identified: {}\", godClass.getFileName());\n                cboClasses.add(godClass);\n            }\n        }\n        return cboClasses;\n    }\n\n    public List<RankedDisharmony> calculateSourceNodeCostBenefitValues(\n            Graph<String, DefaultWeightedEdge> classGraph,\n            Map<DefaultWeightedEdge, CycleNode> edgeSourceNodeInfos,\n            Map<DefaultWeightedEdge, CycleNode> edgeTargetNodeInfos,\n            Map<DefaultWeightedEdge, Integer> edgeToRemoveCycleCounts,\n            Set<String> vertexesToRemove) {\n        List<ScmLogInfo> sourceLogInfos = getRankedChangeProneness(new ArrayList<>(edgeSourceNodeInfos.values()));\n        List<ScmLogInfo> targetLogInfos = getRankedChangeProneness(new ArrayList<>(edgeTargetNodeInfos.values()));\n        List<ScmLogInfo> scmLogInfos = new ArrayList<>(sourceLogInfos.size() + targetLogInfos.size());\n        scmLogInfos.addAll(sourceLogInfos);\n        scmLogInfos.addAll(targetLogInfos);\n\n        Map<String, ScmLogInfo> sourceRankedLogInfosByPath = getRankedLogInfosByPath(scmLogInfos);\n        List<RankedDisharmony> edgesThatNeedToBeRemoved = new ArrayList<>();\n\n        for (Map.Entry<DefaultWeightedEdge, CycleNode> entry : edgeSourceNodeInfos.entrySet()) {\n            String edgeSource = classGraph.getEdgeSource(entry.getKey());\n\n            String edgeSourcePath;\n            if (edgeSource.contains(\"$\")) {\n                edgeSourcePath = classToSourceFilePathMapping.get(edgeSource.substring(0, edgeSource.indexOf(\"$\")));\n            } else {\n                edgeSourcePath = classToSourceFilePathMapping.get(edgeSource);\n            }\n\n            String edgeTarget = classGraph.getEdgeTarget(entry.getKey());\n            String edgeTargetPath;\n            if (edgeTarget.contains(\"$\")) {\n                edgeTargetPath = classToSourceFilePathMapping.get(edgeTarget.substring(0, edgeTarget.indexOf(\"$\")));\n            } else {\n                edgeTargetPath = classToSourceFilePathMapping.get(edgeTarget);\n            }\n\n            String sourceNodeFileName = canonicaliseURIStringForRepoLookup(edgeSourcePath);\n            String targetNodeFileName = canonicaliseURIStringForRepoLookup(edgeTargetPath);\n\n            boolean sourceNodeShouldBeRemoved = vertexesToRemove.contains(edgeSource);\n            boolean targetNodeShouldBeRemoved = vertexesToRemove.contains(edgeTarget);\n\n            ScmLogInfo sourceScmLogInfo = null;\n            if (sourceRankedLogInfosByPath.containsKey(sourceNodeFileName)) {\n                sourceScmLogInfo = sourceRankedLogInfosByPath.get(sourceNodeFileName);\n            }\n\n            ScmLogInfo targetScmLogInfo = null;\n            if (sourceRankedLogInfosByPath.containsKey(sourceNodeFileName)) {\n                targetScmLogInfo = sourceRankedLogInfosByPath.get(targetNodeFileName);\n            }\n\n            RankedDisharmony edgeThatNeedsToBeRemoved = new RankedDisharmony(\n                    edgeSource,\n                    entry.getKey(),\n                    edgeToRemoveCycleCounts.get(entry.getKey()),\n                    (int) classGraph.getEdgeWeight(entry.getKey()),\n                    sourceNodeShouldBeRemoved,\n                    targetNodeShouldBeRemoved,\n                    sourceScmLogInfo,\n                    targetScmLogInfo);\n            edgesThatNeedToBeRemoved.add(edgeThatNeedsToBeRemoved);\n        }\n\n        sortEdgesThatNeedToBeRemoved(edgesThatNeedToBeRemoved);\n\n        // Then subtract edge weight\n        int rawPriority = 1;\n        for (RankedDisharmony rankedDisharmony : edgesThatNeedToBeRemoved) {\n            rankedDisharmony.setRawPriority(rawPriority++);\n        }\n\n        // Push edges with higher weights down in the priority list\n        edgesThatNeedToBeRemoved.sort(Comparator.comparing(RankedDisharmony::getRawPriority));\n\n        // Then set priority\n        int sourceNodePriority = 1;\n        for (RankedDisharmony rankedSourceNodeDisharmony : edgesThatNeedToBeRemoved) {\n            rankedSourceNodeDisharmony.setPriority(sourceNodePriority++);\n        }\n\n        return edgesThatNeedToBeRemoved;\n    }\n\n    static void sortEdgesThatNeedToBeRemoved(List<RankedDisharmony> rankedDisharmonies) {\n        // Sort by impact value\n        // Order by cycle count reversed (highest count bubbles to the top)\n        rankedDisharmonies.sort(Comparator.comparingInt(RankedDisharmony::getCycleCount)\n                .reversed()\n                // then by weight, with lowest weight edges bubbling to the top\n                .thenComparingInt(RankedDisharmony::getEffortRank)\n                // then by change proneness\n                .thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getChangePronenessRank())\n                .thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getEdgeTargetChangePronenessRank())\n                // then if the source node is in the list of nodes to be removed\n                // multiplying by -1 reverses the sort order (reverse doesn't work in chained comparators)\n                .thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getSourceNodeShouldBeRemoved())\n                // then if the target node is in the list of nodes to be removed\n                .thenComparingInt(rankedDisharmony -> -1 * rankedDisharmony.getTargetNodeShouldBeRemoved()));\n    }\n\n    private String getFileName(RuleViolation violation) {\n        String uriString = violation.getFileId().getUriString();\n        return canonicaliseURIStringForRepoLookup(uriString);\n    }\n\n    String canonicaliseURIStringForRepoLookup(String uriString) {\n        if (repositoryPath.startsWith(\"/\") || repositoryPath.startsWith(\"\\\\\")) {\n            return uriString.replace(\"file://\" + repositoryPath.replace(\"\\\\\", \"/\") + \"/\", \"\");\n        }\n        return uriString.replace(\"file:///\" + repositoryPath.replace(\"\\\\\", \"/\") + \"/\", \"\");\n    }\n}\n"
  },
  {
    "path": "cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleNode.java",
    "content": "package org.hjug.cbc;\n\nimport java.time.Instant;\nimport lombok.Data;\nimport org.hjug.git.ScmLogInfo;\nimport org.hjug.metrics.Disharmony;\n\n@Data\npublic class CycleNode implements Disharmony {\n\n    private final String className;\n    private String fileName;\n    private Integer changePronenessRank;\n\n    private Instant firstCommitTime;\n    private Instant mostRecentCommitTime;\n    private Integer commitCount;\n\n    public CycleNode(String className, String fileName) {\n        this.className = className;\n        this.fileName = fileName;\n    }\n\n    public String getPackageName() {\n        return className.substring(0, className.lastIndexOf('.'));\n    }\n\n    public void setScmLogInfo(ScmLogInfo scmLogInfo) {\n        firstCommitTime = Instant.ofEpochSecond(scmLogInfo.getEarliestCommit());\n        mostRecentCommitTime = Instant.ofEpochSecond(scmLogInfo.getMostRecentCommit());\n        commitCount = scmLogInfo.getCommitCount();\n    }\n}\n"
  },
  {
    "path": "cost-benefit-calculator/src/main/java/org/hjug/cbc/CycleRanker.java",
    "content": "package org.hjug.cbc;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport lombok.Getter;\nimport lombok.RequiredArgsConstructor;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.dsm.CircularReferenceChecker;\nimport org.hjug.graphbuilder.CodebaseGraphDTO;\nimport org.hjug.graphbuilder.JavaGraphBuilder;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.AsSubgraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n@RequiredArgsConstructor\n@Slf4j\npublic class CycleRanker {\n\n    private final String repositoryPath;\n    private final JavaGraphBuilder javaGraphBuilder = new JavaGraphBuilder();\n\n    @Getter\n    private Graph<String, DefaultWeightedEdge> classReferencesGraph;\n\n    @Getter\n    private CodebaseGraphDTO codebaseGraphDTO;\n\n    @Getter\n    private Map<String, String> classNamesAndPaths = new HashMap<>();\n\n    @Getter\n    private Map<String, String> fqnsAndPaths = new HashMap<>();\n\n    public void generateClassReferencesGraph(boolean excludeTests, String testSourceDirectory) {\n        try {\n            codebaseGraphDTO = javaGraphBuilder.getCodebaseGraphDTO(repositoryPath, excludeTests, testSourceDirectory);\n\n            classReferencesGraph = codebaseGraphDTO.getClassReferencesGraph();\n\n            loadClassNamesAndPaths();\n\n            /*for (Map.Entry<String, String> stringStringEntry : fqnsAndPaths.entrySet()) {\n                log.info(stringStringEntry.getKey() + \" : \" + stringStringEntry.getValue());\n            }*/\n\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public List<RankedCycle> performCycleAnalysis(boolean excludeTests, String testSourceDirectory) {\n        List<RankedCycle> rankedCycles = new ArrayList<>();\n        try {\n            boolean calculateCycleChurn = false;\n            generateClassReferencesGraph(excludeTests, testSourceDirectory);\n            identifyRankedCycles(rankedCycles);\n            sortRankedCycles(rankedCycles, calculateCycleChurn);\n            setPriorities(rankedCycles);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        return rankedCycles;\n    }\n\n    private void identifyRankedCycles(List<RankedCycle> rankedCycles) throws IOException {\n        CircularReferenceChecker circularReferenceChecker = new CircularReferenceChecker();\n        Map<String, AsSubgraph<String, DefaultWeightedEdge>> cycles =\n                circularReferenceChecker.getCycles(classReferencesGraph);\n        cycles.forEach((vertex, subGraph) -> {\n            // TODO: Calculate min cuts for smaller graphs - has a runtime of O(V^4) for a graph\n            /*Set<DefaultWeightedEdge> minCutEdges;\n            GusfieldGomoryHuCutTree<String, DefaultWeightedEdge> gusfieldGomoryHuCutTree =\n                    new GusfieldGomoryHuCutTree<>(new AsUndirectedGraph<>(subGraph));\n            double minCut = gusfieldGomoryHuCutTree.calculateMinCut();\n            minCutEdges = gusfieldGomoryHuCutTree.getCutEdges();*/\n\n            List<CycleNode> cycleNodes = subGraph.vertexSet().stream()\n                    .map(classInCycle -> new CycleNode(classInCycle, classNamesAndPaths.get(classInCycle)))\n                    //                        .peek(cycleNode -> log.info(cycleNode.toString()))\n                    .collect(Collectors.toList());\n\n            rankedCycles.add(createRankedCycle(vertex, subGraph, cycleNodes, 0.0, new HashSet<>()));\n        });\n    }\n\n    public CycleNode classToCycleNode(String fqnClass) {\n        return new CycleNode(fqnClass, fqnsAndPaths.get(fqnClass));\n    }\n\n    private RankedCycle createRankedCycle(\n            String vertex,\n            AsSubgraph<String, DefaultWeightedEdge> subGraph,\n            List<CycleNode> cycleNodes,\n            double minCut,\n            Set<DefaultWeightedEdge> minCutEdges) {\n\n        return new RankedCycle(vertex, subGraph.vertexSet(), subGraph.edgeSet(), minCut, minCutEdges, cycleNodes);\n    }\n\n    private static void sortRankedCycles(List<RankedCycle> rankedCycles, boolean calculateChurnForCycles) {\n        if (calculateChurnForCycles) {\n            rankedCycles.sort(Comparator.comparing(RankedCycle::getAverageChangeProneness));\n\n            int cpr = 1;\n            for (RankedCycle rankedCycle : rankedCycles) {\n                rankedCycle.setChangePronenessRank(cpr++);\n            }\n        } else {\n            rankedCycles.sort(Comparator.comparing(RankedCycle::getRawPriority).reversed());\n        }\n    }\n\n    private static void setPriorities(List<RankedCycle> rankedCycles) {\n        int priority = 1;\n        for (RankedCycle rankedCycle : rankedCycles) {\n            rankedCycle.setPriority(priority++);\n        }\n    }\n\n    void loadClassNamesAndPaths() throws IOException {\n        try (Stream<Path> walk = Files.walk(Paths.get(repositoryPath))) {\n            walk.forEach(path -> {\n                String filename = path.getFileName().toString();\n                if (filename.endsWith(\".java\")) {\n                    // extract package and class name\n                    String packageName = getPackageName(path);\n                    String uriString = path.toUri().toString();\n                    String className = getClassName(filename);\n                    String canonicalUri = canonicaliseURIStringForRepoLookup(uriString);\n                    fqnsAndPaths.put(packageName + \".\" + className, canonicalUri);\n                    classNamesAndPaths.put(className, canonicalUri);\n                }\n            });\n        }\n    }\n\n    private static String getPackageName(Path path) {\n        try {\n            return Files.readAllLines(path).stream()\n                    .filter(line -> line.startsWith(\"package\"))\n                    .map(line -> line.replace(\"package\", \"\").replace(\";\", \"\").trim())\n                    .findFirst()\n                    .orElse(\"\");\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private String canonicaliseURIStringForRepoLookup(String uriString) {\n        if (repositoryPath.startsWith(\"/\") || repositoryPath.startsWith(\"\\\\\")) {\n            return uriString.replace(\"file://\" + repositoryPath.replace(\"\\\\\", \"/\") + \"/\", \"\");\n        }\n        return uriString.replace(\"file:///\" + repositoryPath.replace(\"\\\\\", \"/\") + \"/\", \"\");\n    }\n\n    /**\n     * Extract class name from java file name\n     * Example : MyJavaClass.java becomes MyJavaClass\n     *\n     * @param javaFileName\n     * @return\n     */\n    private String getClassName(String javaFileName) {\n        return javaFileName.substring(0, javaFileName.indexOf('.'));\n    }\n}\n"
  },
  {
    "path": "cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedCycle.java",
    "content": "package org.hjug.cbc;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport lombok.Data;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n@Data\n@Slf4j\npublic class RankedCycle {\n\n    private final String cycleName;\n    private Integer changePronenessRankSum = 0;\n\n    private final Set<String> vertexSet;\n    private final Set<DefaultWeightedEdge> edgeSet;\n    private final double minCutCount;\n    private final Set<DefaultWeightedEdge> minCutEdges;\n    private final List<CycleNode> cycleNodes;\n\n    private float rawPriority;\n    private Integer priority = 0;\n    private float averageChangeProneness;\n    private Integer changePronenessRank = 0;\n    private float impact;\n\n    public RankedCycle(\n            String cycleName,\n            Set<String> vertexSet,\n            Set<DefaultWeightedEdge> edgeSet,\n            double minCutCount,\n            Set<DefaultWeightedEdge> minCutEdges,\n            List<CycleNode> cycleNodes) {\n        this.cycleNodes = cycleNodes;\n        this.cycleName = cycleName;\n        this.vertexSet = vertexSet;\n        this.edgeSet = edgeSet;\n        this.minCutCount = minCutCount;\n\n        if (null == minCutEdges) {\n            this.minCutEdges = new HashSet<>();\n        } else {\n            this.minCutEdges = minCutEdges;\n        }\n\n        if (minCutCount == 0.0) {\n            this.impact = (float) (vertexSet.size());\n        } else {\n            this.impact = (float) (vertexSet.size() / minCutCount);\n        }\n\n        this.rawPriority = this.impact;\n    }\n\n    public RankedCycle(\n            String cycleName,\n            Integer changePronenessRankSum,\n            Set<String> vertexSet,\n            Set<DefaultWeightedEdge> edgeSet,\n            double minCutCount,\n            Set<DefaultWeightedEdge> minCutEdges,\n            List<CycleNode> cycleNodes) {\n        this.cycleNodes = cycleNodes;\n        this.cycleName = cycleName;\n        this.changePronenessRankSum = changePronenessRankSum;\n        this.vertexSet = vertexSet;\n        this.edgeSet = edgeSet;\n        this.minCutCount = minCutCount;\n\n        if (null == minCutEdges) {\n            this.minCutEdges = new HashSet<>();\n        } else {\n            this.minCutEdges = minCutEdges;\n        }\n\n        if (minCutCount == 0.0) {\n            this.impact = (float) (vertexSet.size());\n        } else {\n            this.impact = (float) (vertexSet.size() / minCutCount);\n        }\n\n        this.averageChangeProneness = (float) changePronenessRankSum / vertexSet.size();\n        this.rawPriority = this.impact + averageChangeProneness;\n    }\n}\n"
  },
  {
    "path": "cost-benefit-calculator/src/main/java/org/hjug/cbc/RankedDisharmony.java",
    "content": "package org.hjug.cbc;\n\nimport java.nio.file.Paths;\nimport java.time.Instant;\nimport lombok.Data;\nimport org.hjug.git.ScmLogInfo;\nimport org.hjug.metrics.CBOClass;\nimport org.hjug.metrics.GodClass;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n@Data\npublic class RankedDisharmony {\n\n    private Instant firstCommitTime;\n    private Instant mostRecentCommitTime;\n    private Integer commitCount;\n\n    private String path;\n    private String fileName;\n    private final String className;\n    private final Integer effortRank;\n    private final Integer changePronenessRank;\n    private Integer rawPriority;\n    private Integer priority = 0;\n\n    private Integer wmc;\n    private Integer wmcRank;\n    private Integer atfd;\n    private Integer atfdRank;\n    private Float tcc;\n    private Integer tccRank;\n\n    private DefaultWeightedEdge edge;\n    private Integer cycleCount;\n    private int sourceNodeShouldBeRemoved;\n    private int targetNodeShouldBeRemoved;\n    private String edgeTargetClass;\n    private Integer edgeTargetChangePronenessRank;\n\n    public RankedDisharmony(GodClass godClass, ScmLogInfo scmLogInfo) {\n        path = scmLogInfo.getPath();\n        // from https://stackoverflow.com/questions/1011287/get-file-name-from-a-file-location-in-java\n        fileName = Paths.get(path).getFileName().toString();\n        className = godClass.getClassName();\n        changePronenessRank = scmLogInfo.getChangePronenessRank();\n        effortRank = godClass.getOverallRank();\n        rawPriority = changePronenessRank - effortRank;\n\n        wmc = godClass.getWmc();\n        wmcRank = godClass.getWmcRank();\n        atfd = godClass.getAtfd();\n        atfdRank = godClass.getAtfdRank();\n        tcc = godClass.getTcc();\n        tccRank = godClass.getTccRank();\n\n        firstCommitTime = Instant.ofEpochSecond(scmLogInfo.getEarliestCommit());\n        mostRecentCommitTime = Instant.ofEpochSecond(scmLogInfo.getMostRecentCommit());\n        commitCount = scmLogInfo.getCommitCount();\n    }\n\n    public RankedDisharmony(CBOClass cboClass, ScmLogInfo scmLogInfo) {\n        path = scmLogInfo.getPath();\n        // from https://stackoverflow.com/questions/1011287/get-file-name-from-a-file-location-in-java\n        fileName = Paths.get(path).getFileName().toString();\n        className = cboClass.getClassName();\n        changePronenessRank = scmLogInfo.getChangePronenessRank();\n        effortRank = cboClass.getCouplingCount();\n        rawPriority = changePronenessRank - effortRank;\n\n        firstCommitTime = Instant.ofEpochSecond(scmLogInfo.getEarliestCommit());\n        mostRecentCommitTime = Instant.ofEpochSecond(scmLogInfo.getMostRecentCommit());\n        commitCount = scmLogInfo.getCommitCount();\n    }\n\n    public RankedDisharmony(\n            String edgeSource,\n            DefaultWeightedEdge edge,\n            int cycleCount,\n            int weight,\n            boolean sourceNodeShouldBeRemoved,\n            boolean targetNodeShouldBeRemoved,\n            ScmLogInfo sourceScmLogInfo,\n            ScmLogInfo targetScmLogInfo) {\n\n        if (null != sourceScmLogInfo) {\n            path = sourceScmLogInfo.getPath();\n            // from https://stackoverflow.com/questions/1011287/get-file-name-from-a-file-location-in-java\n            fileName = Paths.get(path).getFileName().toString();\n            firstCommitTime = Instant.ofEpochSecond(sourceScmLogInfo.getEarliestCommit());\n            mostRecentCommitTime = Instant.ofEpochSecond(sourceScmLogInfo.getMostRecentCommit());\n            commitCount = sourceScmLogInfo.getCommitCount();\n        }\n\n        className = edgeSource;\n        this.edge = edge;\n        this.cycleCount = cycleCount;\n        changePronenessRank = null == sourceScmLogInfo ? 0 : sourceScmLogInfo.getChangePronenessRank();\n        edgeTargetChangePronenessRank = null == targetScmLogInfo ? 0 : targetScmLogInfo.getChangePronenessRank();\n        effortRank = weight;\n        this.sourceNodeShouldBeRemoved = sourceNodeShouldBeRemoved ? 1 : 0;\n        this.targetNodeShouldBeRemoved = targetNodeShouldBeRemoved ? 1 : 0;\n    }\n}\n"
  },
  {
    "path": "cost-benefit-calculator/src/test/java/org/hjug/cbc/CostBenefitCalculatorTest.java",
    "content": "package org.hjug.cbc;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\nimport java.io.*;\nimport java.util.*;\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.api.errors.GitAPIException;\nimport org.eclipse.jgit.lib.Repository;\nimport org.eclipse.jgit.revwalk.RevCommit;\nimport org.hjug.git.ScmLogInfo;\nimport org.hjug.metrics.Disharmony;\nimport org.jetbrains.annotations.NotNull;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.graph.SimpleDirectedWeightedGraph;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.io.TempDir;\n\nclass CostBenefitCalculatorTest {\n\n    @TempDir\n    public File tempFolder;\n\n    private String faceletsPath = \"org/apache/myfaces/tobago/facelets/\";\n    private String hudsonPath = \"hudson/model/\";\n    private Git git;\n    private Repository repository;\n\n    @BeforeEach\n    public void setUp() throws GitAPIException {\n        git = Git.init().setDirectory(tempFolder).call();\n        repository = git.getRepository();\n        new File(tempFolder.getPath() + \"/\" + faceletsPath).mkdirs();\n        new File(tempFolder.getPath() + \"/\" + hudsonPath).mkdirs();\n    }\n\n    @AfterEach\n    public void tearDown() {\n        repository.close();\n    }\n\n    @Test\n    void testCBOViolation() throws IOException, GitAPIException, InterruptedException {\n        // Has CBO violation\n        String user = \"User.java\";\n        InputStream userResourceAsStream = getClass().getClassLoader().getResourceAsStream(hudsonPath + user);\n        writeFile(hudsonPath + user, convertInputStreamToString(userResourceAsStream));\n\n        git.add().addFilepattern(\".\").call();\n        RevCommit firstCommit = git.commit().setMessage(\"message\").call();\n\n        CostBenefitCalculator costBenefitCalculator =\n                new CostBenefitCalculator(git.getRepository().getDirectory().getParent(), new HashMap<>());\n        costBenefitCalculator.runPmdAnalysis();\n        List<RankedDisharmony> disharmonies = costBenefitCalculator.calculateCBOCostBenefitValues();\n\n        Assertions.assertFalse(disharmonies.isEmpty());\n    }\n\n    @Test\n    void testCostBenefitCalculation() throws IOException, GitAPIException, InterruptedException {\n\n        String attributeHandler = \"AttributeHandler.java\";\n        InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(faceletsPath + attributeHandler);\n        writeFile(faceletsPath + attributeHandler, convertInputStreamToString(resourceAsStream));\n\n        git.add().addFilepattern(\".\").call();\n        RevCommit firstCommit = git.commit().setMessage(\"message\").call();\n\n        // Sleeping for one second to guarantee commits have different time stamps\n        Thread.sleep(1000);\n\n        // write contents of updated file to original file\n        InputStream resourceAsStream2 =\n                getClass().getClassLoader().getResourceAsStream(faceletsPath + \"AttributeHandler2.java\");\n        writeFile(faceletsPath + attributeHandler, convertInputStreamToString(resourceAsStream2));\n\n        InputStream resourceAsStream3 =\n                getClass().getClassLoader().getResourceAsStream(faceletsPath + \"AttributeHandlerAndSorter.java\");\n        writeFile(faceletsPath + \"AttributeHandlerAndSorter.java\", convertInputStreamToString(resourceAsStream3));\n\n        git.add().addFilepattern(\".\").call();\n        RevCommit secondCommit = git.commit().setMessage(\"message\").call();\n\n        CostBenefitCalculator costBenefitCalculator =\n                new CostBenefitCalculator(git.getRepository().getDirectory().getParent(), new HashMap<>());\n        costBenefitCalculator.runPmdAnalysis();\n        List<RankedDisharmony> disharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues();\n\n        Assertions.assertEquals(1, disharmonies.get(0).getRawPriority().intValue());\n        Assertions.assertEquals(1, disharmonies.get(1).getRawPriority().intValue());\n\n        Assertions.assertEquals(1, disharmonies.get(0).getPriority().intValue());\n        Assertions.assertEquals(2, disharmonies.get(1).getPriority().intValue());\n    }\n\n    @Test\n    void calculateSourceNodeCostBenefitValues_filtersMissingLogInfoAndAssignsPriority() throws Exception {\n\n        writeFile(hudsonPath + \"Dummy.java\", \"public class Dummy {}\");\n\n        git.add().addFilepattern(\".\").call();\n        git.commit().setMessage(\"initial commit\").call();\n\n        SimpleDirectedWeightedGraph<String, DefaultWeightedEdge> classGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        classGraph.addVertex(\"ClassA\");\n        classGraph.addVertex(\"ClassB\");\n        classGraph.addVertex(\"ClassC\");\n        classGraph.addVertex(\"ClassD\");\n        classGraph.addVertex(\"ClassE\");\n        classGraph.addVertex(\"ClassF\");\n\n        DefaultWeightedEdge edge1 = classGraph.addEdge(\"ClassA\", \"ClassB\");\n        classGraph.setEdgeWeight(edge1, 4);\n\n        DefaultWeightedEdge edge2 = classGraph.addEdge(\"ClassC\", \"ClassD\");\n        classGraph.setEdgeWeight(edge2, 2);\n\n        Map<DefaultWeightedEdge, CycleNode> edgeSourceNodeInfos = new HashMap<>();\n        edgeSourceNodeInfos.put(edge1, new CycleNode(\"ClassA\", hudsonPath + \"ClassA.java\"));\n        edgeSourceNodeInfos.put(edge2, new CycleNode(\"ClassC\", hudsonPath + \"ClassC.java\"));\n\n        Map<DefaultWeightedEdge, CycleNode> edgeTargeteNodeInfos = new HashMap<>();\n        edgeTargeteNodeInfos.put(edge1, new CycleNode(\"ClassB\", hudsonPath + \"ClassB.java\"));\n        edgeTargeteNodeInfos.put(edge2, new CycleNode(\"ClassD\", hudsonPath + \"ClassD.java\"));\n\n        Map<DefaultWeightedEdge, Integer> edgeToRemoveCycleCounts = new HashMap<>();\n        edgeToRemoveCycleCounts.put(edge1, 5);\n        edgeToRemoveCycleCounts.put(edge2, 3);\n\n        Set<String> vertexesToRemove = new HashSet<>(Arrays.asList(\"ClassA\", \"ClassD\"));\n\n        ScmLogInfo scmLogInfo1 = new ScmLogInfo(hudsonPath + \"ClassA.java\", null, 1, 2, 3);\n        scmLogInfo1.setChangePronenessRank(4);\n\n        ScmLogInfo scmLogInfo2 = new ScmLogInfo(hudsonPath + \"ClassC.java\", null, 1, 2, 5);\n        scmLogInfo2.setChangePronenessRank(7);\n\n        List<ScmLogInfo> scmLogInfos = Arrays.asList(scmLogInfo1, scmLogInfo2);\n\n        try (TestableCostBenefitCalculator costBenefitCalculator = new TestableCostBenefitCalculator(\n                git.getRepository().getDirectory().getParent(), scmLogInfos)) {\n\n            List<RankedDisharmony> disharmonies = costBenefitCalculator.calculateSourceNodeCostBenefitValues(\n                    classGraph, edgeTargeteNodeInfos, edgeTargeteNodeInfos, edgeToRemoveCycleCounts, vertexesToRemove);\n\n            Assertions.assertEquals(2, disharmonies.size());\n\n            RankedDisharmony classA = disharmonies.get(0);\n            Assertions.assertEquals(\"ClassA\", classA.getClassName());\n            Assertions.assertEquals(5, classA.getCycleCount().intValue());\n            Assertions.assertEquals(4, classA.getEffortRank().intValue());\n            Assertions.assertEquals(1, classA.getSourceNodeShouldBeRemoved());\n            Assertions.assertEquals(0, classA.getTargetNodeShouldBeRemoved());\n            Assertions.assertEquals(1, classA.getPriority().intValue());\n\n            RankedDisharmony classC = disharmonies.get(1);\n            Assertions.assertEquals(\"ClassC\", classC.getClassName());\n            Assertions.assertEquals(3, classC.getCycleCount().intValue());\n            Assertions.assertEquals(2, classC.getEffortRank().intValue());\n            Assertions.assertEquals(0, classC.getSourceNodeShouldBeRemoved());\n            Assertions.assertEquals(1, classC.getTargetNodeShouldBeRemoved());\n            Assertions.assertEquals(2, classC.getPriority().intValue());\n            Assertions.assertEquals(7, classC.getChangePronenessRank());\n        }\n    }\n\n    @Test\n    void calculateSourceNodeCostBenefitValues_prefersHigherChangePronenessRank() throws Exception {\n\n        writeFile(faceletsPath + \"Placeholder.java\", \"public class Placeholder {}\");\n\n        git.add().addFilepattern(\".\").call();\n        git.commit().setMessage(\"initial commit\").call();\n\n        SimpleDirectedWeightedGraph<String, DefaultWeightedEdge> classGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n\n        classGraph.addVertex(\"Alpha\");\n        classGraph.addVertex(\"Beta\");\n        classGraph.addVertex(\"Gamma\");\n\n        DefaultWeightedEdge edge1 = classGraph.addEdge(\"Alpha\", \"Beta\");\n        classGraph.setEdgeWeight(edge1, 3);\n\n        DefaultWeightedEdge edge2 = classGraph.addEdge(\"Gamma\", \"Beta\");\n        classGraph.setEdgeWeight(edge2, 3);\n\n        Map<DefaultWeightedEdge, CycleNode> edgeSourceNodeInfos = new HashMap<>();\n        edgeSourceNodeInfos.put(edge1, new CycleNode(\"Alpha\", faceletsPath + \"Alpha.java\"));\n        edgeSourceNodeInfos.put(edge2, new CycleNode(\"Gamma\", faceletsPath + \"Gamma.java\"));\n\n        Map<DefaultWeightedEdge, CycleNode> edgeTargetNodeInfos = new HashMap<>();\n        edgeTargetNodeInfos.put(edge1, new CycleNode(\"Beta\", faceletsPath + \"Beta.java\"));\n\n        Map<DefaultWeightedEdge, Integer> edgeToRemoveCycleCounts = new HashMap<>();\n        edgeToRemoveCycleCounts.put(edge1, 4);\n        edgeToRemoveCycleCounts.put(edge2, 4);\n\n        Set<String> vertexesToRemove = new HashSet<>(Arrays.asList(\"Alpha\", \"Gamma\"));\n\n        ScmLogInfo scmLogInfo1 = new ScmLogInfo(faceletsPath + \"Alpha.java\", null, 2, 3, 1);\n        scmLogInfo1.setChangePronenessRank(2);\n\n        ScmLogInfo scmLogInfo2 = new ScmLogInfo(faceletsPath + \"Gamma.java\", null, 2, 3, 1);\n        scmLogInfo2.setChangePronenessRank(8);\n\n        List<ScmLogInfo> scmLogInfos = Arrays.asList(scmLogInfo1, scmLogInfo2);\n\n        try (TestableCostBenefitCalculator costBenefitCalculator = new TestableCostBenefitCalculator(\n                git.getRepository().getDirectory().getParent(), scmLogInfos)) {\n\n            List<RankedDisharmony> disharmonies = costBenefitCalculator.calculateSourceNodeCostBenefitValues(\n                    classGraph, edgeSourceNodeInfos, edgeTargetNodeInfos, edgeToRemoveCycleCounts, vertexesToRemove);\n\n            Assertions.assertEquals(2, disharmonies.size());\n            Assertions.assertEquals(8, disharmonies.get(0).getChangePronenessRank());\n            Assertions.assertEquals(1, disharmonies.get(0).getPriority().intValue());\n            Assertions.assertEquals(2, disharmonies.get(1).getPriority().intValue());\n            Assertions.assertEquals(2, disharmonies.get(1).getChangePronenessRank());\n        }\n    }\n\n    @Test\n    void sortEdgesThatNeedToBeRemoved_sortsByMultipleCriteria() {\n        // Create ScmLogInfo objects for testing\n        ScmLogInfo logInfo1 = new ScmLogInfo(\"path1.java\", null, 1, 2, 3);\n        logInfo1.setChangePronenessRank(5);\n\n        ScmLogInfo logInfo2 = new ScmLogInfo(\"path2.java\", null, 1, 2, 3);\n        logInfo2.setChangePronenessRank(3);\n\n        ScmLogInfo logInfo3 = new ScmLogInfo(\"path3.java\", null, 1, 2, 3);\n        logInfo3.setChangePronenessRank(8);\n\n        ScmLogInfo logInfo4 = new ScmLogInfo(\"path4.java\", null, 1, 2, 3);\n        logInfo4.setChangePronenessRank(2);\n\n        ScmLogInfo logInfo5 = new ScmLogInfo(\"path4.java\", null, 1, 2, 3);\n        logInfo5.setChangePronenessRank(5);\n\n        // Create RankedDisharmony objects with different combinations\n        // Expected order after sorting: cycleCount desc, then sourceRemoved desc, then targetRemoved desc, then\n        // changeProneness desc\n        // cycle=5, source=0, target=0, change=5\n        RankedDisharmony disharmony1 = new RankedDisharmony(\n                \"Class1\", new org.jgrapht.graph.DefaultWeightedEdge(), 5, 1, false, false, logInfo1, null);\n\n        // cycle=5, source=1, target=0, change=3\n        RankedDisharmony disharmony2 = new RankedDisharmony(\n                \"Class2\", new org.jgrapht.graph.DefaultWeightedEdge(), 5, 1, true, false, logInfo2, null);\n\n        // cycle=3, source=0, target=1, change=8\n        RankedDisharmony disharmony3 = new RankedDisharmony(\n                \"Class3\", new org.jgrapht.graph.DefaultWeightedEdge(), 3, 1, false, true, logInfo3, null);\n\n        // cycle=3, source=0, target=0, change=2\n        RankedDisharmony disharmony4 = new RankedDisharmony(\n                \"Class4\", new org.jgrapht.graph.DefaultWeightedEdge(), 3, 1, false, false, logInfo4, null);\n\n        // cycle=3, source=0, target=0, change=5\n        RankedDisharmony disharmony5 = new RankedDisharmony(\n                \"Class5\", new org.jgrapht.graph.DefaultWeightedEdge(), 3, 1, false, false, logInfo5, null);\n\n        List<RankedDisharmony> disharmonies =\n                Arrays.asList(disharmony4, disharmony2, disharmony1, disharmony3, disharmony5);\n\n        // Sort the list\n        CostBenefitCalculator.sortEdgesThatNeedToBeRemoved(disharmonies);\n\n        // Verify the order\n        // Order by cycle count reversed (highest count bubbles to the top)\n        // then Order by source node removed (source nodes needing to be removed bubble to the top)\n        // then Order by target node removed (target nodes needing to be removed bubble to the top)\\\n        // then Order by change proneness (highest change proneness bubbles to the top)\n        for (RankedDisharmony disharmony : disharmonies) {\n            System.out.println(disharmony.getClassName() + \" \"\n                    + disharmony.getCycleCount() + \" \"\n                    + disharmony.getEffortRank() + \" \"\n                    + disharmony.getSourceNodeShouldBeRemoved() + \" \"\n                    + disharmony.getTargetNodeShouldBeRemoved() + \" \"\n                    + disharmony.getChangePronenessRank());\n        }\n\n        RankedDisharmony orderedDisharmony0 = disharmonies.get(0);\n        Assertions.assertEquals(\"Class1\", orderedDisharmony0.getClassName());\n        Assertions.assertEquals(5, orderedDisharmony0.getCycleCount().intValue());\n        Assertions.assertEquals(1, orderedDisharmony0.getEffortRank().intValue());\n        Assertions.assertEquals(0, orderedDisharmony0.getSourceNodeShouldBeRemoved());\n        Assertions.assertEquals(0, orderedDisharmony0.getTargetNodeShouldBeRemoved());\n        Assertions.assertEquals(5, orderedDisharmony0.getChangePronenessRank());\n\n        RankedDisharmony orderedDisharmony1 = disharmonies.get(1);\n        Assertions.assertEquals(\"Class2\", orderedDisharmony1.getClassName());\n        Assertions.assertEquals(5, orderedDisharmony1.getCycleCount().intValue());\n        Assertions.assertEquals(1, orderedDisharmony1.getEffortRank().intValue());\n        Assertions.assertEquals(1, orderedDisharmony1.getSourceNodeShouldBeRemoved());\n        Assertions.assertEquals(0, orderedDisharmony1.getTargetNodeShouldBeRemoved());\n        Assertions.assertEquals(3, orderedDisharmony1.getChangePronenessRank());\n\n        RankedDisharmony orderedDisharmony2 = disharmonies.get(2);\n        Assertions.assertEquals(\"Class3\", orderedDisharmony2.getClassName());\n        Assertions.assertEquals(3, orderedDisharmony2.getCycleCount().intValue());\n        Assertions.assertEquals(1, orderedDisharmony2.getEffortRank().intValue());\n        Assertions.assertEquals(0, orderedDisharmony2.getSourceNodeShouldBeRemoved());\n        Assertions.assertEquals(1, orderedDisharmony2.getTargetNodeShouldBeRemoved());\n        Assertions.assertEquals(8, orderedDisharmony2.getChangePronenessRank());\n\n        RankedDisharmony orderedDisharmony3 = disharmonies.get(3);\n        Assertions.assertEquals(\"Class5\", orderedDisharmony3.getClassName());\n        Assertions.assertEquals(3, orderedDisharmony3.getCycleCount().intValue());\n        Assertions.assertEquals(1, orderedDisharmony3.getEffortRank().intValue());\n        Assertions.assertEquals(0, orderedDisharmony3.getSourceNodeShouldBeRemoved());\n        Assertions.assertEquals(0, orderedDisharmony3.getTargetNodeShouldBeRemoved());\n        Assertions.assertEquals(5, orderedDisharmony3.getChangePronenessRank());\n\n        RankedDisharmony orderedDisharmony4 = disharmonies.get(4);\n        Assertions.assertEquals(\"Class4\", orderedDisharmony4.getClassName());\n        Assertions.assertEquals(1, orderedDisharmony4.getEffortRank().intValue());\n        Assertions.assertEquals(3, orderedDisharmony4.getCycleCount().intValue());\n        Assertions.assertEquals(0, orderedDisharmony4.getSourceNodeShouldBeRemoved());\n        Assertions.assertEquals(0, orderedDisharmony4.getTargetNodeShouldBeRemoved());\n        Assertions.assertEquals(2, orderedDisharmony4.getChangePronenessRank());\n    }\n\n    private void writeFile(String name, String content) throws IOException {\n        // Files.writeString(Path.of(git.getRepository().getWorkTree().getPath()), content);\n        File file = new File(git.getRepository().getWorkTree(), name);\n\n        try (FileOutputStream outputStream = new FileOutputStream(file)) {\n            outputStream.write(content.getBytes(UTF_8));\n        }\n    }\n\n    private String convertInputStreamToString(InputStream inputStream) throws IOException {\n        ByteArrayOutputStream result = new ByteArrayOutputStream();\n        byte[] buffer = new byte[1024];\n        int length;\n        while ((length = inputStream.read(buffer)) != -1) {\n            result.write(buffer, 0, length);\n        }\n        return result.toString(\"UTF-8\");\n    }\n\n    private static class TestableCostBenefitCalculator extends CostBenefitCalculator {\n\n        private final List<ScmLogInfo> scmLogInfos;\n\n        TestableCostBenefitCalculator(String repositoryPath, List<ScmLogInfo> scmLogInfos) {\n            super(repositoryPath, getClassToSourceFilePathMapping());\n            this.scmLogInfos = scmLogInfos;\n        }\n\n        private static @NotNull Map<String, String> getClassToSourceFilePathMapping() {\n            Map<String, String> classToSourceFilePathMapping = new HashMap<>();\n            classToSourceFilePathMapping.put(\"Alpha\", \"org/apache/myfaces/tobago/facelets/Alpha.java\");\n            classToSourceFilePathMapping.put(\"Beta\", \"org/apache/myfaces/tobago/facelets/Beta.java\");\n            classToSourceFilePathMapping.put(\"Gamma\", \"org/apache/myfaces/tobago/facelets/Gamma.java\");\n            classToSourceFilePathMapping.put(\"ClassA\", \"hudson/model/ClassA.java\");\n            classToSourceFilePathMapping.put(\"ClassB\", \"hudson/model/ClassB.java\");\n            classToSourceFilePathMapping.put(\"ClassC\", \"hudson/model/ClassC.java\");\n            classToSourceFilePathMapping.put(\"ClassD\", \"hudson/model/ClassD.java\");\n            return classToSourceFilePathMapping;\n        }\n\n        @Override\n        public <T extends Disharmony> List<ScmLogInfo> getRankedChangeProneness(List<T> disharmonies) {\n            return new ArrayList<>(scmLogInfos);\n        }\n    }\n}\n"
  },
  {
    "path": "cost-benefit-calculator/src/test/resources/hudson/model/User.java",
    "content": "/*\n * The MIT License\n *\n * Copyright (c) 2004-2018, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt,\n * Tom Huybrechts, Vincent Latombe, CloudBees, Inc.\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\npackage hudson.model;\n\nimport com.infradna.tool.bridge_method_injector.WithBridgeMethods;\nimport edu.umd.cs.findbugs.annotations.CheckForNull;\nimport edu.umd.cs.findbugs.annotations.NonNull;\nimport edu.umd.cs.findbugs.annotations.Nullable;\nimport edu.umd.cs.findbugs.annotations.SuppressFBWarnings;\nimport hudson.BulkChange;\nimport hudson.CopyOnWrite;\nimport hudson.Extension;\nimport hudson.ExtensionList;\nimport hudson.ExtensionPoint;\nimport hudson.Util;\nimport hudson.XmlFile;\nimport hudson.init.InitMilestone;\nimport hudson.init.Initializer;\nimport hudson.model.Descriptor.FormException;\nimport hudson.model.listeners.SaveableListener;\nimport hudson.security.ACL;\nimport hudson.security.AccessControlled;\nimport hudson.security.SecurityRealm;\nimport hudson.security.UserMayOrMayNotExistException2;\nimport hudson.util.FormApply;\nimport hudson.util.FormValidation;\nimport hudson.util.RunList;\nimport hudson.util.XStream2;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport java.util.concurrent.ExecutionException;\nimport java.util.function.Predicate;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServletResponse;\nimport jenkins.model.IdStrategy;\nimport jenkins.model.Jenkins;\nimport jenkins.model.ModelObjectWithContextMenu;\nimport jenkins.scm.RunWithSCM;\nimport jenkins.security.ImpersonatingUserDetailsService2;\nimport jenkins.security.LastGrantedAuthoritiesProperty;\nimport jenkins.security.UserDetailsCache;\nimport jenkins.util.SystemProperties;\nimport net.sf.json.JSONObject;\nimport org.apache.commons.lang.StringUtils;\nimport org.jenkinsci.Symbol;\nimport org.kohsuke.accmod.Restricted;\nimport org.kohsuke.accmod.restrictions.NoExternalUse;\nimport org.kohsuke.stapler.StaplerProxy;\nimport org.kohsuke.stapler.StaplerRequest;\nimport org.kohsuke.stapler.StaplerResponse;\nimport org.kohsuke.stapler.export.Exported;\nimport org.kohsuke.stapler.export.ExportedBean;\nimport org.kohsuke.stapler.interceptor.RequirePOST;\nimport org.kohsuke.stapler.verb.POST;\nimport org.springframework.security.authentication.AnonymousAuthenticationToken;\nimport org.springframework.security.authentication.UsernamePasswordAuthenticationToken;\nimport org.springframework.security.core.Authentication;\nimport org.springframework.security.core.AuthenticationException;\nimport org.springframework.security.core.GrantedAuthority;\nimport org.springframework.security.core.userdetails.UserDetails;\nimport org.springframework.security.core.userdetails.UsernameNotFoundException;\n\n/**\n * Represents a user.\n *\n * <p>\n * In Hudson, {@link User} objects are created in on-demand basis;\n * for example, when a build is performed, its change log is computed\n * and as a result commits from users who Hudson has never seen may be discovered.\n * When this happens, new {@link User} object is created.\n *\n * <p>\n * If the persisted record for an user exists, the information is loaded at\n * that point, but if there's no such record, a fresh instance is created from\n * thin air (this is where {@link UserPropertyDescriptor#newInstance(User)} is\n * called to provide initial {@link UserProperty} objects.\n *\n * <p>\n * Such newly created {@link User} objects will be simply GC-ed without\n * ever leaving the persisted record, unless {@link User#save()} method\n * is explicitly invoked (perhaps as a result of a browser submitting a\n * configuration.)\n *\n * @author Kohsuke Kawaguchi\n */\n@ExportedBean\npublic class User extends AbstractModelObject implements AccessControlled, DescriptorByNameOwner, Saveable, Comparable<User>, ModelObjectWithContextMenu, StaplerProxy {\n\n    public static final XStream2 XSTREAM = new XStream2();\n    private static final Logger LOGGER = Logger.getLogger(User.class.getName());\n    static final String CONFIG_XML = \"config.xml\";\n\n    /**\n     * Escape hatch for StaplerProxy-based access control\n     */\n    @Restricted(NoExternalUse.class)\n    @SuppressFBWarnings(value = \"MS_SHOULD_BE_FINAL\", justification = \"for script console\")\n    public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = SystemProperties.getBoolean(User.class.getName() + \".skipPermissionCheck\");\n\n    /**\n     * Jenkins now refuses to let the user login if he/she doesn't exist in {@link SecurityRealm},\n     * which was necessary to make sure users removed from the backend will get removed from the frontend.\n     * <p>\n     * Unfortunately this infringed some legitimate use cases of creating Jenkins-local users for\n     * automation purposes. This escape hatch switch can be enabled to resurrect that behaviour.\n     * <p>\n     * See <a href=\"https://issues.jenkins.io/browse/JENKINS-22346\">JENKINS-22346</a>.\n     */\n    @SuppressFBWarnings(value = \"MS_SHOULD_BE_FINAL\", justification = \"for script console\")\n    public static boolean ALLOW_NON_EXISTENT_USER_TO_LOGIN = SystemProperties.getBoolean(User.class.getName() + \".allowNonExistentUserToLogin\");\n\n    /**\n     * Jenkins historically created a (usually) ephemeral user record when an user with Overall/Administer permission\n     * accesses a /user/arbitraryName URL.\n     * <p>\n     * Unfortunately this constitutes a CSRF vulnerability, as malicious users can make admins create arbitrary numbers\n     * of ephemeral user records, so the behavior was changed in Jenkins 2.44 / 2.32.2.\n     * <p>\n     * As some users may be relying on the previous behavior, setting this to true restores the previous behavior. This\n     * is not recommended.\n     * <p>\n     * SECURITY-406.\n     */\n    @Restricted(NoExternalUse.class)\n    @SuppressFBWarnings(value = \"MS_SHOULD_BE_FINAL\", justification = \"for script console\")\n    public static boolean ALLOW_USER_CREATION_VIA_URL = SystemProperties.getBoolean(User.class.getName() + \".allowUserCreationViaUrl\");\n\n    /**\n     * The username of the 'unknown' user used to avoid null user references.\n     */\n    private static final String UNKNOWN_USERNAME = \"unknown\";\n\n    /**\n     * These usernames should not be used by real users logging into Jenkins. Therefore, we prevent\n     * users with these names from being saved.\n     */\n    private static final String[] ILLEGAL_PERSISTED_USERNAMES = new String[]{ACL.ANONYMOUS_USERNAME,\n            ACL.SYSTEM_USERNAME, UNKNOWN_USERNAME};\n\n    private final int version = 10; // Not currently used, but it may be helpful in the future to store a version.\n    private String id;\n    private volatile String fullName;\n    private volatile String description;\n\n    @CopyOnWrite\n    private volatile List<UserProperty> properties = new ArrayList<>();\n\n    static {\n        XSTREAM.alias(\"user\", User.class);\n    }\n\n    private User(String id, String fullName) {\n        this.id = id;\n        this.fullName = fullName;\n        load(id);\n    }\n\n    private void load(String userId) {\n        clearExistingProperties();\n        loadFromUserConfigFile(userId);\n        removeNullsThatFailedToLoad();\n        allocateDefaultPropertyInstancesAsNeeded();\n        setUserToProperties();\n    }\n\n    private void setUserToProperties() {\n        for (UserProperty p : properties) {\n            p.setUser(this);\n        }\n    }\n\n    private void allocateDefaultPropertyInstancesAsNeeded() {\n        for (UserPropertyDescriptor d : UserProperty.all()) {\n            if (getProperty(d.clazz) == null) {\n                UserProperty up = d.newInstance(this);\n                if (up != null)\n                    properties.add(up);\n            }\n        }\n    }\n\n    private void removeNullsThatFailedToLoad() {\n        properties.removeIf(Objects::isNull);\n    }\n\n    private void loadFromUserConfigFile(String userId) {\n        XmlFile config = getConfigFile();\n        try {\n            if (config != null && config.exists()) {\n                config.unmarshal(this);\n                this.id = userId;\n            }\n        } catch (IOException e) {\n            LOGGER.log(Level.SEVERE, \"Failed to load \" + config, e);\n        }\n    }\n\n    private void clearExistingProperties() {\n        properties.clear();\n    }\n\n    private XmlFile getConfigFile() {\n        File existingUserFolder = getExistingUserFolder();\n        return existingUserFolder == null ? null : new XmlFile(XSTREAM, new File(existingUserFolder, CONFIG_XML));\n    }\n\n    /**\n     * Returns the {@link jenkins.model.IdStrategy} for use with {@link User} instances. See\n     * {@link hudson.security.SecurityRealm#getUserIdStrategy()}\n     *\n     * @return the {@link jenkins.model.IdStrategy} for use with {@link User} instances.\n     * @since 1.566\n     */\n    @NonNull\n    public static IdStrategy idStrategy() {\n        Jenkins j = Jenkins.get();\n        SecurityRealm realm = j.getSecurityRealm();\n        if (realm == null) {\n            return IdStrategy.CASE_INSENSITIVE;\n        }\n        return realm.getUserIdStrategy();\n    }\n\n    @Override\n    public int compareTo(@NonNull User that) {\n        return idStrategy().compare(this.id, that.id);\n    }\n\n    @Exported\n    public String getId() {\n        return id;\n    }\n\n    public @NonNull String getUrl() {\n        return \"user/\" + Util.rawEncode(idStrategy().keyFor(id));\n    }\n\n    @Override\n    public @NonNull String getSearchUrl() {\n        return \"/user/\" + Util.rawEncode(idStrategy().keyFor(id));\n    }\n\n    /**\n     * The URL of the user page.\n     */\n    @Exported(visibility = 999)\n    public @NonNull String getAbsoluteUrl() {\n        return Jenkins.get().getRootUrl() + getUrl();\n    }\n\n    /**\n     * Gets the human readable name of this user.\n     * This is configurable by the user.\n     */\n    @Exported(visibility = 999)\n    public @NonNull String getFullName() {\n        return fullName;\n    }\n\n    /**\n     * Sets the human readable name of the user.\n     * If the input parameter is empty, the user's ID will be set.\n     */\n    public void setFullName(String name) {\n        if (Util.fixEmptyAndTrim(name) == null) name = id;\n        this.fullName = name;\n    }\n\n    @Exported\n    public @CheckForNull String getDescription() {\n        return description;\n    }\n\n    /**\n     * Sets the description of the user.\n     *\n     * @since 1.609\n     */\n    public void setDescription(String description) {\n        this.description = description;\n    }\n\n    /**\n     * Gets the user properties configured for this user.\n     */\n    public Map<Descriptor<UserProperty>, UserProperty> getProperties() {\n        return Descriptor.toMap(properties);\n    }\n\n    /**\n     * Updates the user object by adding a property.\n     */\n    public synchronized void addProperty(@NonNull UserProperty p) throws IOException {\n        UserProperty old = getProperty(p.getClass());\n        List<UserProperty> ps = new ArrayList<>(properties);\n        if (old != null)\n            ps.remove(old);\n        ps.add(p);\n        p.setUser(this);\n        properties = ps;\n        save();\n    }\n\n    /**\n     * List of all {@link UserProperty}s exposed primarily for the remoting API.\n     */\n    @Exported(name = \"property\", inline = true)\n    public List<UserProperty> getAllProperties() {\n        if (hasPermission(Jenkins.ADMINISTER)) {\n            return Collections.unmodifiableList(properties);\n        }\n\n        return Collections.emptyList();\n    }\n\n    /**\n     * Gets the specific property, or null.\n     */\n    public <T extends UserProperty> T getProperty(Class<T> clazz) {\n        for (UserProperty p : properties) {\n            if (clazz.isInstance(p))\n                return clazz.cast(p);\n        }\n        return null;\n    }\n\n    /**\n     * Creates an {@link Authentication} object that represents this user.\n     * <p>\n     * This method checks with {@link SecurityRealm} if the user is a valid user that can login to the security realm.\n     * If {@link SecurityRealm} is a kind that does not support querying information about other users, this will\n     * use {@link LastGrantedAuthoritiesProperty} to pick up the granted authorities as of the last time the user has\n     * logged in.\n     *\n     * @throws UsernameNotFoundException If this user is not a valid user in the backend {@link SecurityRealm}.\n     * @since 2.266\n     */\n    public @NonNull Authentication impersonate2() throws UsernameNotFoundException {\n        return this.impersonate(this.getUserDetailsForImpersonation2());\n    }\n\n    /**\n     * @deprecated use {@link #impersonate2}\n     * @since 1.419\n     */\n    @Deprecated\n    public @NonNull org.acegisecurity.Authentication impersonate() throws org.acegisecurity.userdetails.UsernameNotFoundException {\n        try {\n            return org.acegisecurity.Authentication.fromSpring(impersonate2());\n        } catch (AuthenticationException x) {\n            throw org.acegisecurity.AuthenticationException.fromSpring(x);\n        }\n    }\n\n    /**\n     * This method checks with {@link SecurityRealm} if the user is a valid user that can login to the security realm.\n     * If {@link SecurityRealm} is a kind that does not support querying information about other users, this will\n     * use {@link LastGrantedAuthoritiesProperty} to pick up the granted authorities as of the last time the user has\n     * logged in.\n     *\n     * @return userDetails for the user, in case he's not found but seems legitimate, we provide a userDetails with minimum access\n     * @throws UsernameNotFoundException If this user is not a valid user in the backend {@link SecurityRealm}.\n     * @since 2.266\n     */\n    public @NonNull UserDetails getUserDetailsForImpersonation2() throws UsernameNotFoundException {\n        ImpersonatingUserDetailsService2 userDetailsService = new ImpersonatingUserDetailsService2(\n                Jenkins.get().getSecurityRealm().getSecurityComponents().userDetails2\n        );\n\n        try {\n            UserDetails userDetails = userDetailsService.loadUserByUsername(id);\n            LOGGER.log(Level.FINE, \"Impersonation of the user {0} was a success\", id);\n            return userDetails;\n        } catch (UserMayOrMayNotExistException2 e) {\n            LOGGER.log(Level.FINE, \"The user {0} may or may not exist in the SecurityRealm, so we provide minimum access\", id);\n        } catch (UsernameNotFoundException e) {\n            if (ALLOW_NON_EXISTENT_USER_TO_LOGIN) {\n                LOGGER.log(Level.FINE, \"The user {0} was not found in the SecurityRealm but we are required to let it pass, due to ALLOW_NON_EXISTENT_USER_TO_LOGIN\", id);\n            } else {\n                LOGGER.log(Level.FINE, \"The user {0} was not found in the SecurityRealm\", id);\n                throw e;\n            }\n        }\n\n        return new LegitimateButUnknownUserDetails(id);\n    }\n\n    /**\n     * @deprecated use {@link #getUserDetailsForImpersonation2}\n     */\n    @Deprecated\n    public @NonNull org.acegisecurity.userdetails.UserDetails getUserDetailsForImpersonation() throws org.acegisecurity.userdetails.UsernameNotFoundException {\n        try {\n            return org.acegisecurity.userdetails.UserDetails.fromSpring(getUserDetailsForImpersonation2());\n        } catch (AuthenticationException x) {\n            throw org.acegisecurity.AuthenticationException.fromSpring(x);\n        }\n    }\n\n    /**\n     * Only used for a legitimate user we have no idea about. We give it only minimum access\n     */\n    private static class LegitimateButUnknownUserDetails extends org.springframework.security.core.userdetails.User {\n        private LegitimateButUnknownUserDetails(String username) throws IllegalArgumentException {\n            super(\n                    username, \"\",\n                    true, true, true, true,\n                    Set.of(SecurityRealm.AUTHENTICATED_AUTHORITY2)\n            );\n        }\n    }\n\n    /**\n     * Creates an {@link Authentication} object that represents this user using the given userDetails\n     *\n     * @param userDetails Provided by {@link #getUserDetailsForImpersonation2()}.\n     * @see #getUserDetailsForImpersonation2()\n     */\n    @Restricted(NoExternalUse.class)\n    public @NonNull Authentication impersonate(@NonNull UserDetails userDetails) {\n        return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), \"\", userDetails.getAuthorities());\n    }\n\n    /**\n     * Accepts the new description.\n     */\n    @RequirePOST\n    public void doSubmitDescription(StaplerRequest req, StaplerResponse rsp) throws IOException {\n        checkPermission(Jenkins.ADMINISTER);\n\n        description = req.getParameter(\"description\");\n        save();\n\n        rsp.sendRedirect(\".\");  // go to the top page\n    }\n\n    /**\n     * Gets the fallback \"unknown\" user instance.\n     * <p>\n     * This is used to avoid null {@link User} instance.\n     */\n    public static @NonNull User getUnknown() {\n        return getById(UNKNOWN_USERNAME, true);\n    }\n\n    /**\n     * Gets the {@link User} object by its id or full name.\n     *\n     * @param create If true, this method will never return null for valid input\n     *               (by creating a new {@link User} object if none exists.)\n     *               If false, this method will return null if {@link User} object\n     *               with the given name doesn't exist.\n     * @return Requested user. May be {@code null} if a user does not exist and\n     * {@code create} is false.\n     * @deprecated use {@link User#get(String, boolean, java.util.Map)}\n     */\n    @Deprecated\n    public static @Nullable User get(String idOrFullName, boolean create) {\n        return get(idOrFullName, create, Collections.emptyMap());\n    }\n\n    /**\n     * Gets the {@link User} object by its id or full name.\n     * <p>\n     * In order to resolve the user ID, the method invokes {@link CanonicalIdResolver} extension points.\n     * Note that it may cause significant performance degradation.\n     * If you are sure the passed value is a User ID, it is recommended to use {@link #getById(String, boolean)}.\n     *\n     * @param create  If true, this method will never return null for valid input\n     *                (by creating a new {@link User} object if none exists.)\n     *                If false, this method will return null if {@link User} object\n     *                with the given name doesn't exist.\n     * @param context contextual environment this user idOfFullName was retrieved from,\n     *                that can help resolve the user ID\n     * @return An existing or created user. May be {@code null} if a user does not exist and\n     * {@code create} is false.\n     */\n    public static @Nullable User get(String idOrFullName, boolean create, @NonNull Map context) {\n        if (idOrFullName == null) {\n            return null;\n        }\n\n        User user = AllUsers.get(idOrFullName);\n        if (user != null) {\n            return user;\n        }\n\n        String id = CanonicalIdResolver.resolve(idOrFullName, context);\n        return getOrCreateById(id, idOrFullName, create);\n    }\n\n    /**\n     * Retrieve a user by its ID, and create a new one if requested.\n     *\n     * @return An existing or created user. May be {@code null} if a user does not exist and\n     * {@code create} is false.\n     */\n    private static @Nullable User getOrCreateById(@NonNull String id, @NonNull String fullName, boolean create) {\n        User u = AllUsers.get(id);\n        if (u == null && (create || UserIdMapper.getInstance().isMapped(id))) {\n            u = new User(id, fullName);\n            AllUsers.put(id, u);\n            if (!id.equals(fullName) && !UserIdMapper.getInstance().isMapped(id)) {\n                try {\n                    u.save();\n                } catch (IOException x) {\n                    LOGGER.log(Level.WARNING, \"Failed to save user configuration for \" + id, x);\n                }\n            }\n        }\n        return u;\n    }\n\n    /**\n     * Gets the {@link User} object by its id or full name.\n     * <p>\n     * Creates a user on-demand.\n     *\n     * <p>\n     * Use {@link #getById} when you know you have an ID.\n     * In this method Jenkins will try to resolve the {@link User} by full name with help of various\n     * {@link hudson.tasks.UserNameResolver}.\n     * This is slow (see JENKINS-23281).\n     *\n     * @deprecated This method is deprecated, because it causes unexpected {@link User} creation\n     * by API usage code and causes performance degradation of used to retrieve users by ID.\n     * Use {@link #getById} when you know you have an ID.\n     * Otherwise use {@link #getOrCreateByIdOrFullName(String)} or {@link #get(String, boolean, Map)}.\n     */\n    @Deprecated\n    public static @NonNull User get(String idOrFullName) {\n        return getOrCreateByIdOrFullName(idOrFullName);\n    }\n\n    /**\n     * Get the user by ID or Full Name.\n     * <p>\n     * If the user does not exist, creates a new one on-demand.\n     *\n     * <p>\n     * Use {@link #getById} when you know you have an ID.\n     * In this method Jenkins will try to resolve the {@link User} by full name with help of various\n     * {@link hudson.tasks.UserNameResolver}.\n     * This is slow (see JENKINS-23281).\n     *\n     * @param idOrFullName User ID or full name\n     * @return User instance. It will be created on-demand.\n     * @since 2.91\n     */\n    public static @NonNull User getOrCreateByIdOrFullName(@NonNull String idOrFullName) {\n        return get(idOrFullName, true, Collections.emptyMap());\n    }\n\n\n    /**\n     * Gets the {@link User} object representing the currently logged-in user, or null\n     * if the current user is anonymous.\n     *\n     * @since 1.172\n     */\n    public static @CheckForNull User current() {\n        return get2(Jenkins.getAuthentication2());\n    }\n\n    /**\n     * Gets the {@link User} object representing the supplied {@link Authentication} or\n     * {@code null} if the supplied {@link Authentication} is either anonymous or {@code null}\n     *\n     * @param a the supplied {@link Authentication} .\n     * @return a {@link User} object for the supplied {@link Authentication} or {@code null}\n     * @since 2.266\n     */\n    public static @CheckForNull User get2(@CheckForNull Authentication a) {\n        if (a == null || a instanceof AnonymousAuthenticationToken)\n            return null;\n\n        // Since we already know this is a name, we can just call getOrCreateById with the name directly.\n        return getById(a.getName(), true);\n    }\n\n    /**\n     * @deprecated use {@link #get2(Authentication)}\n     * @since 1.609\n     */\n    @Deprecated\n    public static @CheckForNull User get(@CheckForNull org.acegisecurity.Authentication a) {\n        return get2(a != null ? a.toSpring() : null);\n    }\n\n    /**\n     * Gets the {@link User} object by its {@code id}\n     *\n     * @param id     the id of the user to retrieve and optionally create if it does not exist.\n     * @param create If {@code true}, this method will never return {@code null} for valid input (by creating a\n     *               new {@link User} object if none exists.) If {@code false}, this method will return\n     *               {@code null} if {@link User} object with the given id doesn't exist.\n     * @return the a User whose id is {@code id}, or {@code null} if {@code create} is {@code false}\n     * and the user does not exist.\n     * @since 1.651.2 / 2.3\n     */\n    public static @Nullable User getById(String id, boolean create) {\n        return getOrCreateById(id, id, create);\n    }\n\n    /**\n     * Gets all the users.\n     */\n    public static @NonNull Collection<User> getAll() {\n        final IdStrategy strategy = idStrategy();\n        ArrayList<User> users = new ArrayList<>(AllUsers.values());\n        users.sort((o1, o2) -> strategy.compare(o1.getId(), o2.getId()));\n        return users;\n    }\n\n    /**\n     * To be called from {@link Jenkins#reload} only.\n     */\n    @Restricted(NoExternalUse.class)\n    public static void reload() throws IOException {\n        UserIdMapper.getInstance().reload();\n        AllUsers.reload();\n    }\n\n    /**\n     * Called when changing the {@link IdStrategy}.\n     *\n     * @since 1.566\n     */\n    public static void rekey() {\n        /* There are many and varied ways in which this could cause erratic or\n            problematic behavior. Such changes should really only occur during initial\n            setup and under very controlled situations. After this sort of a change\n            the whole webapp should restart. It's possible that this rekeying,\n            or greater issues in the realm change, could affect currently logged\n            in users and even the user making the change. */\n        try {\n            reload();\n        } catch (IOException e) {\n            LOGGER.log(Level.SEVERE, \"Failed to perform rekey operation.\", e);\n        }\n    }\n\n    /**\n     * Returns the user name.\n     */\n    @Override\n    public @NonNull String getDisplayName() {\n        return getFullName();\n    }\n\n    /**\n     * true if {@link RunWithSCM#hasParticipant} or {@link hudson.model.Cause.UserIdCause}\n     */\n    private boolean relatedTo(@NonNull Run<?, ?> b) {\n        if (b instanceof RunWithSCM && ((RunWithSCM) b).hasParticipant(this)) {\n            return true;\n        }\n        for (Cause cause : b.getCauses()) {\n            if (cause instanceof Cause.UserIdCause) {\n                String userId = ((Cause.UserIdCause) cause).getUserId();\n                if (userId != null && idStrategy().equals(userId, getId())) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Searches for builds which include changes by this user or which were triggered by this user.\n     */\n    @SuppressWarnings(\"unchecked\")\n    @WithBridgeMethods(List.class)\n    public @NonNull RunList getBuilds() {\n        return RunList.fromJobs((Iterable) Jenkins.get().\n                allItems(Job.class)).filter((Predicate<Run<?, ?>>) this::relatedTo);\n    }\n\n    /**\n     * Gets all the {@link AbstractProject}s that this user has committed to.\n     *\n     * @since 1.191\n     */\n    public @NonNull Set<AbstractProject<?, ?>> getProjects() {\n        Set<AbstractProject<?, ?>> r = new HashSet<>();\n        for (AbstractProject<?, ?> p : Jenkins.get().allItems(AbstractProject.class, p -> p.hasParticipant(this)))\n            r.add(p);\n        return r;\n    }\n\n    @Override\n    public String toString() {\n        return id;\n    }\n\n    /**\n     * Called by tests in the JTH. Otherwise this shouldn't be called.\n     * Even in the tests this usage is questionable.\n     * @deprecated removed without replacement\n     */\n    @Deprecated\n    public static void clear() {\n        if (ExtensionList.lookup(AllUsers.class).isEmpty()) {\n            return;\n        }\n        UserIdMapper.getInstance().clear();\n        AllUsers.clear();\n    }\n\n    private static File getConfigFileFor(String id) {\n        return new File(getUserFolderFor(id), \"config.xml\");\n    }\n\n    private static File getUserFolderFor(String id) {\n        return new File(getRootDir(), idStrategy().filenameOf(id));\n    }\n    /**\n     * Returns the folder that store all the user information.\n     * Useful for plugins to save a user-specific file aside the config.xml.\n     * Exposes implementation details that may be subject to change.\n     *\n     * @return The folder containing the user configuration files or {@code null} if the user was not yet saved.\n     *\n     * @since 2.129\n     */\n\n    public @CheckForNull File getUserFolder() {\n        return getExistingUserFolder();\n    }\n\n    private @CheckForNull File getExistingUserFolder() {\n        return UserIdMapper.getInstance().getDirectory(id);\n    }\n\n    /**\n     * Gets the directory where Hudson stores user information.\n     */\n    static File getRootDir() {\n        return new File(Jenkins.get().getRootDir(), \"users\");\n    }\n\n    /**\n     * Is the ID allowed? Some are prohibited for security reasons. See SECURITY-166.\n     * <p>\n     * Note that this is only enforced when saving. These users are often created\n     * via the constructor (and even listed on /asynchPeople), but our goal is to\n     * prevent anyone from logging in as these users. Therefore, we prevent\n     * saving a User with one of these ids.\n     *\n     * @param id ID to be checked\n     * @return {@code true} if the username or fullname is valid.\n     * For {@code null} or blank IDs returns {@code false}.\n     * @since 1.600\n     */\n    public static boolean isIdOrFullnameAllowed(@CheckForNull String id) {\n        if (StringUtils.isBlank(id)) {\n            return false;\n        }\n        final String trimmedId = id.trim();\n        for (String invalidId : ILLEGAL_PERSISTED_USERNAMES) {\n            if (trimmedId.equalsIgnoreCase(invalidId))\n                return false;\n        }\n        return true;\n    }\n\n    /**\n     * Save the user configuration.\n     */\n    @Override\n    public synchronized void save() throws IOException {\n        if (!isIdOrFullnameAllowed(id)) {\n            throw FormValidation.error(Messages.User_IllegalUsername(id));\n        }\n        if (!isIdOrFullnameAllowed(fullName)) {\n            throw FormValidation.error(Messages.User_IllegalFullname(fullName));\n        }\n        if (BulkChange.contains(this)) {\n            return;\n        }\n        XmlFile xmlFile = new XmlFile(XSTREAM, constructUserConfigFile());\n        xmlFile.write(this);\n        SaveableListener.fireOnChange(this, xmlFile);\n    }\n\n    private File constructUserConfigFile() throws IOException {\n        return new File(putUserFolderIfAbsent(), CONFIG_XML);\n    }\n\n    private File putUserFolderIfAbsent() throws IOException {\n        return UserIdMapper.getInstance().putIfAbsent(id, true);\n    }\n\n    /**\n     * Deletes the data directory and removes this user from Hudson.\n     *\n     * @throws IOException if we fail to delete.\n     */\n    public void delete() throws IOException {\n        String idKey = idStrategy().keyFor(id);\n        File existingUserFolder = getExistingUserFolder();\n        UserIdMapper.getInstance().remove(id);\n        AllUsers.remove(id);\n        deleteExistingUserFolder(existingUserFolder);\n        UserDetailsCache.get().invalidate(idKey);\n    }\n\n    private void deleteExistingUserFolder(File existingUserFolder) throws IOException {\n        if (existingUserFolder != null && existingUserFolder.exists()) {\n            Util.deleteRecursive(existingUserFolder);\n        }\n    }\n\n    /**\n     * Exposed remote API.\n     */\n    public Api getApi() {\n        return new Api(this);\n    }\n\n    /**\n     * Accepts submission from the configuration page.\n     */\n    @POST\n    public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {\n        checkPermission(Jenkins.ADMINISTER);\n\n        JSONObject json = req.getSubmittedForm();\n        String oldFullName = this.fullName;\n        fullName = json.getString(\"fullName\");\n        description = json.getString(\"description\");\n\n        List<UserProperty> props = new ArrayList<>();\n        int i = 0;\n        for (UserPropertyDescriptor d : UserProperty.all()) {\n            UserProperty p = getProperty(d.clazz);\n\n            JSONObject o = json.optJSONObject(\"userProperty\" + i++);\n            if (o != null) {\n                if (p != null) {\n                    p = p.reconfigure(req, o);\n                } else {\n                    p = d.newInstance(req, o);\n                }\n                p.setUser(this);\n            }\n\n            if (p != null)\n                props.add(p);\n        }\n        this.properties = props;\n\n        save();\n\n        if (oldFullName != null && !oldFullName.equals(this.fullName)) {\n            UserDetailsCache.get().invalidate(oldFullName);\n        }\n\n        FormApply.success(\".\").generateResponse(req, rsp, this);\n    }\n\n    /**\n     * Deletes this user from Hudson.\n     */\n    @RequirePOST\n    public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException {\n        checkPermission(Jenkins.ADMINISTER);\n        if (idStrategy().equals(id, Jenkins.getAuthentication2().getName())) {\n            rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, \"Cannot delete self\");\n            return;\n        }\n\n        delete();\n\n        rsp.sendRedirect2(\"../..\");\n    }\n\n    public void doRssAll(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n        RSS.rss(req, rsp, \"Jenkins:\" + getDisplayName() + \" (all builds)\", getUrl(), getBuilds().newBuilds());\n    }\n\n    public void doRssFailed(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n        RSS.rss(req, rsp, \"Jenkins:\" + getDisplayName() + \" (failed builds)\", getUrl(), getBuilds().regressionOnly());\n    }\n\n    public void doRssLatest(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {\n        final List<Run> lastBuilds = new ArrayList<>();\n        for (Job<?, ?> p : Jenkins.get().allItems(Job.class)) {\n            for (Run<?, ?> b = p.getLastBuild(); b != null; b = b.getPreviousBuild()) {\n                if (relatedTo(b)) {\n                    lastBuilds.add(b);\n                    break;\n                }\n            }\n        }\n        // historically these have been reported sorted by project name, we switched to the lazy iteration\n        // so we only have to sort the sublist of runs rather than the full list of irrelevant projects\n        lastBuilds.sort((o1, o2) -> Items.BY_FULL_NAME.compare(o1.getParent(), o2.getParent()));\n        RSS.rss(req, rsp, \"Jenkins:\" + getDisplayName() + \" (latest builds)\", getUrl(), RunList.fromRuns(lastBuilds), Run.FEED_ADAPTER_LATEST);\n    }\n\n    @Override\n    @NonNull\n    public ACL getACL() {\n        ACL base = Jenkins.get().getAuthorizationStrategy().getACL(this);\n        // always allow a non-anonymous user full control of himself.\n        return ACL.lambda2((a, permission) -> (idStrategy().equals(a.getName(), id) && !(a instanceof AnonymousAuthenticationToken))\n                || base.hasPermission2(a, permission));\n    }\n\n    /**\n     * With ADMINISTER permission, can delete users with persisted data but can't delete self.\n     */\n    public boolean canDelete() {\n        final IdStrategy strategy = idStrategy();\n        return hasPermission(Jenkins.ADMINISTER) && !strategy.equals(id, Jenkins.getAuthentication2().getName())\n                && UserIdMapper.getInstance().isMapped(id);\n    }\n\n    /**\n     * Checks for authorities (groups) associated with this user.\n     * If the caller lacks {@link Jenkins#ADMINISTER}, or any problems arise, returns an empty list.\n     * {@link SecurityRealm#AUTHENTICATED_AUTHORITY2} and the username, if present, are omitted.\n     *\n     * @return a possibly empty list\n     * @since 1.498\n     */\n    public @NonNull List<String> getAuthorities() {\n        if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {\n            return Collections.emptyList();\n        }\n        List<String> r = new ArrayList<>();\n        Authentication authentication;\n        try {\n            authentication = impersonate2();\n        } catch (UsernameNotFoundException x) {\n            LOGGER.log(Level.FINE, \"cannot look up authorities for \" + id, x);\n            return Collections.emptyList();\n        }\n        for (GrantedAuthority a : authentication.getAuthorities()) {\n            if (a.equals(SecurityRealm.AUTHENTICATED_AUTHORITY2)) {\n                continue;\n            }\n            String n = a.getAuthority();\n            if (n != null && !idStrategy().equals(n, id)) {\n                r.add(n);\n            }\n        }\n        r.sort(String.CASE_INSENSITIVE_ORDER);\n        return r;\n    }\n\n    public Object getDynamic(String token) {\n        for (Action action : getTransientActions()) {\n            if (Objects.equals(action.getUrlName(), token))\n                return action;\n        }\n        for (Action action : getPropertyActions()) {\n            if (Objects.equals(action.getUrlName(), token))\n                return action;\n        }\n        return null;\n    }\n\n    /**\n     * Return all properties that are also actions.\n     *\n     * @return the list can be empty but never null. read only.\n     */\n    public List<Action> getPropertyActions() {\n        List<Action> actions = new ArrayList<>();\n        for (UserProperty userProp : getProperties().values()) {\n            if (userProp instanceof Action) {\n                actions.add((Action) userProp);\n            }\n        }\n        return Collections.unmodifiableList(actions);\n    }\n\n    /**\n     * Return all transient actions associated with this user.\n     *\n     * @return the list can be empty but never null. read only.\n     */\n    public List<Action> getTransientActions() {\n        List<Action> actions = new ArrayList<>();\n        for (TransientUserActionFactory factory : TransientUserActionFactory.all()) {\n            actions.addAll(factory.createFor(this));\n        }\n        return Collections.unmodifiableList(actions);\n    }\n\n    @Override\n    public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception {\n        return new ContextMenu().from(this, request, response);\n    }\n\n    @Override\n    @Restricted(NoExternalUse.class)\n    public Object getTarget() {\n        if (!SKIP_PERMISSION_CHECK) {\n            if (!Jenkins.get().hasPermission(Jenkins.READ)) {\n                return null;\n            }\n        }\n        return this;\n    }\n\n    /**\n     * Gets list of Illegal usernames, for which users should not be created.\n     * Always includes users from {@link #ILLEGAL_PERSISTED_USERNAMES}\n     *\n     * @return List of usernames\n     */\n    @Restricted(NoExternalUse.class)\n    /*package*/ static Set<String> getIllegalPersistedUsernames() {\n        return new HashSet<>(Arrays.asList(ILLEGAL_PERSISTED_USERNAMES));\n    }\n\n    private Object writeReplace() {\n        return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this));\n    }\n\n    private static class Replacer {\n        private final String id;\n\n        Replacer(User u) {\n            id = u.getId();\n        }\n\n        private Object readResolve() {\n            return getById(id, false);\n        }\n    }\n\n    /**\n     * Per-{@link Jenkins} holder of all known {@link User}s.\n     */\n    @Extension\n    @Restricted(NoExternalUse.class)\n    public static final class AllUsers {\n\n        private final ConcurrentMap<String, User> byName = new ConcurrentHashMap<>();\n\n        @Initializer(after = InitMilestone.JOB_CONFIG_ADAPTED)\n        public static void scanAll() {\n            for (String userId : UserIdMapper.getInstance().getConvertedUserIds()) {\n                User user = new User(userId, userId);\n                getInstance().byName.putIfAbsent(idStrategy().keyFor(userId), user);\n            }\n        }\n\n        /**\n         * Keyed by {@link User#id}. This map is used to ensure\n         * singleton-per-id semantics of {@link User} objects.\n         * <p>\n         * The key needs to be generated by {@link IdStrategy#keyFor(String)}.\n         */\n        private static AllUsers getInstance() {\n            return ExtensionList.lookupSingleton(AllUsers.class);\n        }\n\n        private static void reload() {\n            getInstance().byName.clear();\n            UserDetailsCache.get().invalidateAll();\n            scanAll();\n        }\n\n        private static void clear() {\n            getInstance().byName.clear();\n        }\n\n        private static void remove(String id) {\n            getInstance().byName.remove(idStrategy().keyFor(id));\n        }\n\n        private static User get(String id) {\n            return getInstance().byName.get(idStrategy().keyFor(id));\n        }\n\n        private static void put(String id, User user) {\n            getInstance().byName.putIfAbsent(idStrategy().keyFor(id), user);\n        }\n\n        private static Collection<User> values() {\n            return getInstance().byName.values();\n        }\n    }\n\n    /**\n     * Resolves User IDs by ID, full names or other strings.\n     * <p>\n     * This extension point may be useful to map SCM user names to Jenkins {@link User} IDs.\n     * Currently the extension point is used in {@link User#get(String, boolean, Map)}.\n     *\n     * @see jenkins.model.DefaultUserCanonicalIdResolver\n     * @see FullNameIdResolver\n     * @since 1.479\n     */\n    public abstract static class CanonicalIdResolver extends AbstractDescribableImpl<CanonicalIdResolver> implements ExtensionPoint, Comparable<CanonicalIdResolver> {\n\n        /**\n         * context key for realm (domain) where idOrFullName has been retrieved from.\n         * Can be used (for example) to distinguish ambiguous committer ID using the SCM URL.\n         * Associated Value is a {@link String}\n         */\n        public static final String REALM = \"realm\";\n\n        @Override\n        public int compareTo(@NonNull CanonicalIdResolver o) {\n            // reverse priority order\n            return Integer.compare(o.getPriority(), getPriority());\n        }\n\n        /**\n         * extract user ID from idOrFullName with help from contextual infos.\n         * can return {@code null} if no user ID matched the input\n         */\n        public abstract @CheckForNull String resolveCanonicalId(String idOrFullName, Map<String, ?> context);\n\n        /**\n         * Gets priority of the resolver.\n         * Higher priority means that it will be checked earlier.\n         * <p>\n         * Overriding methods must not use {@link Integer#MIN_VALUE}, because it will cause collisions\n         * with {@link jenkins.model.DefaultUserCanonicalIdResolver}.\n         *\n         * @return Priority of the resolver.\n         */\n        public int getPriority() {\n            return 1;\n        }\n\n        //Such sorting and collection rebuild is not good for User#get(...) method performance.\n\n        /**\n         * Gets all extension points, sorted by priority.\n         *\n         * @return Sorted list of extension point implementations.\n         * @since 2.93\n         */\n        public static List<CanonicalIdResolver> all() {\n            List<CanonicalIdResolver> resolvers = new ArrayList<>(ExtensionList.lookup(CanonicalIdResolver.class));\n            Collections.sort(resolvers);\n            return resolvers;\n        }\n\n        /**\n         * Resolves users using all available {@link CanonicalIdResolver}s.\n         *\n         * @param idOrFullName ID or full name of the user\n         * @param context      Context\n         * @return Resolved User ID or {@code null} if the user ID cannot be resolved.\n         * @since 2.93\n         */\n        @CheckForNull\n        public static String resolve(@NonNull String idOrFullName, @NonNull Map<String, ?> context) {\n            for (CanonicalIdResolver resolver : CanonicalIdResolver.all()) {\n                String id = resolver.resolveCanonicalId(idOrFullName, context);\n                if (id != null) {\n                    LOGGER.log(Level.FINE, \"{0} mapped {1} to {2}\", new Object[]{resolver, idOrFullName, id});\n                    return id;\n                }\n            }\n\n            // De-facto it is not going to happen OOTB, because the current DefaultUserCanonicalIdResolver\n            // always returns a value. But we still need to check nulls if somebody disables the extension point\n            return null;\n        }\n    }\n\n\n    /**\n     * Resolve user ID from full name\n     */\n    @Extension\n    @Symbol(\"fullName\")\n    public static class FullNameIdResolver extends CanonicalIdResolver {\n\n        @Override\n        public String resolveCanonicalId(String idOrFullName, Map<String, ?> context) {\n            for (User user : getAll()) {\n                if (idOrFullName.equals(user.getFullName())) return user.getId();\n            }\n            return null;\n        }\n\n        @Override\n        public int getPriority() {\n            return -1; // lower than default\n        }\n    }\n\n\n    /**\n     * Tries to verify if an ID is valid.\n     * If so, we do not want to even consider users who might have the same full name.\n     */\n    @Extension\n    @Restricted(NoExternalUse.class)\n    public static class UserIDCanonicalIdResolver extends User.CanonicalIdResolver {\n\n        private static /* not final */ boolean SECURITY_243_FULL_DEFENSE =\n                SystemProperties.getBoolean(User.class.getName() + \".SECURITY_243_FULL_DEFENSE\", true);\n\n        private static final ThreadLocal<Boolean> resolving = ThreadLocal.withInitial(() -> false);\n\n        @Override\n        public String resolveCanonicalId(String idOrFullName, Map<String, ?> context) {\n            User existing = getById(idOrFullName, false);\n            if (existing != null) {\n                return existing.getId();\n            }\n            if (SECURITY_243_FULL_DEFENSE) {\n                if (!resolving.get()) {\n                    resolving.set(true);\n                    try {\n                        UserDetails userDetails = UserDetailsCache.get().loadUserByUsername(idOrFullName);\n                        return userDetails.getUsername();\n                    } catch (UsernameNotFoundException x) {\n                        LOGGER.log(Level.FINE, \"not sure whether \" + idOrFullName + \" is a valid username or not\", x);\n                    } catch (ExecutionException x) {\n                        LOGGER.log(Level.FINE, \"could not look up \" + idOrFullName, x);\n                    } finally {\n                        resolving.set(false);\n                    }\n                }\n            }\n            return null;\n        }\n\n        @Override\n        public int getPriority() {\n            // should always come first so that ID that are ids get mapped correctly\n            return Integer.MAX_VALUE;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler.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.myfaces.tobago.facelets;\n\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.apache.myfaces.tobago.component.Attributes;\nimport org.apache.myfaces.tobago.component.SupportsMarkup;\nimport org.apache.myfaces.tobago.component.SupportsRenderedPartially;\nimport org.apache.myfaces.tobago.context.Markup;\nimport org.apache.myfaces.tobago.el.ConstantMethodBinding;\nimport org.apache.myfaces.tobago.internal.util.StringUtils;\nimport org.apache.myfaces.tobago.util.ComponentUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ELException;\nimport javax.el.ExpressionFactory;\nimport javax.el.MethodExpression;\nimport javax.el.ValueExpression;\nimport javax.faces.FacesException;\nimport javax.faces.component.ActionSource;\nimport javax.faces.component.ActionSource2;\nimport javax.faces.component.EditableValueHolder;\nimport javax.faces.component.UIComponent;\nimport javax.faces.component.ValueHolder;\nimport javax.faces.convert.Converter;\nimport javax.faces.event.MethodExpressionActionListener;\nimport javax.faces.event.MethodExpressionValueChangeListener;\nimport javax.faces.validator.MethodExpressionValidator;\nimport javax.faces.view.facelets.ComponentHandler;\nimport javax.faces.view.facelets.FaceletContext;\nimport javax.faces.view.facelets.TagAttribute;\nimport javax.faces.view.facelets.TagConfig;\nimport javax.faces.view.facelets.TagException;\nimport javax.faces.view.facelets.TagHandler;\nimport java.beans.IntrospectionException;\nimport java.beans.PropertyDescriptor;\n\n//from Apache MyFaces 2.0.8\n//Retrieved from http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/facelets/AttributeHandler.java/?v=source\npublic final class AttributeHandler extends TagHandler {\n\n    private static final Logger LOG = LoggerFactory.getLogger(AttributeHandler.class);\n\n    private final TagAttribute name;\n\n    private final TagAttribute value;\n\n    private final TagAttribute mode;\n\n    public AttributeHandler(final TagConfig config) {\n        super(config);\n        this.name = getRequiredAttribute(Attributes.NAME);\n        this.value = getRequiredAttribute(Attributes.VALUE);\n        this.mode = getAttribute(Attributes.MODE);\n    }\n\n    public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {\n        if (parent == null) {\n            throw new TagException(tag, \"Parent UIComponent was null\");\n        }\n\n        if (ComponentHandler.isNew(parent)) {\n\n            if (mode != null) {\n                if (\"isNotSet\".equals(mode.getValue())) {\n                    boolean result = false;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = true;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = false;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"isSet\".equals(mode.getValue())) {\n                    boolean result = true;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = false;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = true;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isNotEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"action\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                // when the action hasn't been set while using a composition.\n                                if (LOG.isDebugEnabled()) {\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (\"actionListener\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                if (LOG.isDebugEnabled()) {\n                                    // when the action hasn't been set while using a composition.\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            LOG.warn(\"Only expressions are supported mode=actionListener value='\" + expressionString + \"'\");\n                            expressionString = null;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression actionListener\n                                = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));\n                    }\n                } else if (\"actionFromValue\".equals(mode.getValue())) {\n                    if (!value.isLiteral()) {\n                        final String result = value.getValue(faceletContext);\n                        parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));\n                    }\n                } else if (\"valueIfSet\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    String lastExpressionString = null;\n                    while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {\n                        final ValueExpression expression\n                                = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                        if (expression != null) {\n                            lastExpressionString = expressionString;\n                            expressionString = expression.getExpressionString();\n                        } else {\n                            // restore last value\n                            expressionString = lastExpressionString;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final String attributeName = name.getValue(faceletContext);\n                        if (containsMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(attributeName, expression);\n                        } else {\n                            final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);\n                            parent.getAttributes().put(attributeName, literalValue);\n                        }\n                    }\n                } else {\n                    throw new FacesException(\"Type \" + mode + \" not supported\");\n                }\n            } else {\n\n                final String nameValue = name.getValue(faceletContext);\n                if (Attributes.RENDERED.equals(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.setRendered(value.getBoolean(faceletContext));\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));\n                    }\n                } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)\n                        && parent instanceof SupportsRenderedPartially) {\n\n                    if (value.isLiteral()) {\n                        final String[] components = ComponentUtils.splitList(value.getValue());\n                        ((SupportsRenderedPartially) parent).setRenderedPartially(components);\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                } else if (Attributes.STYLE_CLASS.equals(nameValue)) {\n                    // TODO expression\n                    ComponentUtils.setStyleClasses(parent, value.getValue());\n                } else if (Attributes.MARKUP.equals(nameValue)) {\n                    if (parent instanceof SupportsMarkup) {\n                        if (value.isLiteral()) {\n                            ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));\n                        } else {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(nameValue, expression);\n                        }\n                    } else {\n                        LOG.error(\"Component is not instanceof SupportsMarkup. Instance is: \" + parent.getClass().getName());\n                    }\n                } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {\n                    final MethodExpression methodExpression\n                            = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));\n                    }\n                } else if (parent instanceof EditableValueHolder\n                        && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {\n                    final MethodExpression methodExpression =\n                            getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValueChangeListener(\n                                new MethodExpressionValueChangeListener(methodExpression));\n                    }\n                } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {\n                    setConverter(faceletContext, parent, nameValue);\n                } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {\n                    final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);\n                    if (action != null) {\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {\n                    final MethodExpression action\n                            = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);\n                    if (action != null) {\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));\n                    }\n                } else if (!parent.getAttributes().containsKey(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.getAttributes().put(nameValue, value.getValue());\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                }\n            }\n        }\n    }\n\n    private boolean isMethodOrValueExpression(final String string) {\n        return (string.startsWith(\"${\") || string.startsWith(\"#{\")) && string.endsWith(\"}\");\n    }\n\n    private boolean containsMethodOrValueExpression(final String string) {\n        return (string.contains(\"${\") || string.contains(\"#{\")) && string.contains(\"}\");\n    }\n\n    private boolean isSimpleExpression(final String string) {\n        return string.indexOf('.') < 0 && string.indexOf('[') < 0;\n    }\n\n    private String removeElParenthesis(final String string) {\n        return string.substring(2, string.length() - 1);\n    }\n\n    private ValueExpression getExpression(final FaceletContext faceletContext) {\n        final String myValue = removeElParenthesis(value.getValue());\n        return faceletContext.getVariableMapper().resolveVariable(myValue);\n    }\n\n    private MethodExpression getMethodExpression(\n            final FaceletContext faceletContext, final Class returnType, final Class[] args) {\n        // in a composition may be we get the method expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,\n                        expression.getExpressionString(), returnType, args));\n            } else {\n                return null;\n            }\n        } else {\n            return value.getMethodExpression(faceletContext, returnType, args);\n        }\n    }\n\n    private Object getValue(\n            final FaceletContext faceletContext, final UIComponent parent, final String expressionString,\n            final String attributeName) {\n        Class type = Object.class;\n        try {\n            type = PropertyUtils.getReadMethod(\n                    new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();\n        } catch (final IntrospectionException e) {\n            LOG.warn(\"Can't determine expected type\", e);\n        }\n        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n        final ValueExpression valueExpression = expressionFactory\n                .createValueExpression(faceletContext, expressionString, type);\n        return valueExpression.getValue(faceletContext);\n    }\n\n    private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {\n        // in a composition may be we get the converter expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                setConverter(faceletContext, parent, nameValue, expression);\n            }\n        } else {\n            setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));\n        }\n    }\n\n    private void setConverter(\n            final FaceletContext faceletContext, final UIComponent parent, final String nameValue,\n            final ValueExpression expression) {\n        if (expression.isLiteralText()) {\n            final Converter converter =\n                    faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());\n            ((ValueHolder) parent).setConverter(converter);\n        } else {\n            parent.setValueExpression(nameValue, expression);\n        }\n    }\n}"
  },
  {
    "path": "cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandler2.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.myfaces.tobago.facelets;\n\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.apache.myfaces.tobago.component.Attributes;\nimport org.apache.myfaces.tobago.component.SupportsMarkup;\nimport org.apache.myfaces.tobago.component.SupportsRenderedPartially;\nimport org.apache.myfaces.tobago.context.Markup;\nimport org.apache.myfaces.tobago.el.ConstantMethodBinding;\nimport org.apache.myfaces.tobago.internal.util.StringUtils;\nimport org.apache.myfaces.tobago.util.ComponentUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ELException;\nimport javax.el.ExpressionFactory;\nimport javax.el.MethodExpression;\nimport javax.el.ValueExpression;\nimport javax.faces.FacesException;\nimport javax.faces.component.ActionSource;\nimport javax.faces.component.ActionSource2;\nimport javax.faces.component.EditableValueHolder;\nimport javax.faces.component.UIComponent;\nimport javax.faces.component.ValueHolder;\nimport javax.faces.convert.Converter;\nimport javax.faces.event.MethodExpressionActionListener;\nimport javax.faces.event.MethodExpressionValueChangeListener;\nimport javax.faces.validator.MethodExpressionValidator;\nimport javax.faces.view.facelets.ComponentHandler;\nimport javax.faces.view.facelets.FaceletContext;\nimport javax.faces.view.facelets.TagAttribute;\nimport javax.faces.view.facelets.TagConfig;\nimport javax.faces.view.facelets.TagException;\nimport javax.faces.view.facelets.TagHandler;\nimport java.beans.IntrospectionException;\nimport java.beans.PropertyDescriptor;\n\n//from Apache MyFaces 2.0.8\n//Retrieved from http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/facelets/AttributeHandler.java/?v=source\npublic final class AttributeHandler extends TagHandler {\n\n    private static final Logger LOG = LoggerFactory.getLogger(AttributeHandler.class);\n\n    private final TagAttribute name;\n\n    private final TagAttribute value;\n\n    private final TagAttribute mode;\n\n    public AttributeHandler(final TagConfig config) {\n        super(config);\n        this.name = getRequiredAttribute(Attributes.NAME);\n        this.value = getRequiredAttribute(Attributes.VALUE);\n        this.mode = getAttribute(Attributes.MODE);\n    }\n\n    public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {\n        if (parent == null) {\n            throw new TagException(tag, \"Parent UIComponent was null\");\n        }\n\n        if (ComponentHandler.isNew(parent)) {\n\n            if (mode != null) {\n                if (\"isNotSet\".equals(mode.getValue())) {\n                    boolean result = false;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = true;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = false;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"isSet\".equals(mode.getValue())) {\n                    boolean result = true;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = false;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = true;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isNotEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"action\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                // when the action hasn't been set while using a composition.\n                                if (LOG.isDebugEnabled()) {\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (\"actionListener\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                if (LOG.isDebugEnabled()) {\n                                    // when the action hasn't been set while using a composition.\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            LOG.warn(\"Only expressions are supported mode=actionListener value='\" + expressionString + \"'\");\n                            expressionString = null;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression actionListener\n                                = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));\n                    }\n                } else if (\"actionFromValue\".equals(mode.getValue())) {\n                    if (!value.isLiteral()) {\n                        final String result = value.getValue(faceletContext);\n                        parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));\n                    }\n                } else if (\"valueIfSet\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    String lastExpressionString = null;\n                    while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {\n                        final ValueExpression expression\n                                = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                        if (expression != null) {\n                            lastExpressionString = expressionString;\n                            expressionString = expression.getExpressionString();\n                        } else {\n                            // restore last value\n                            expressionString = lastExpressionString;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final String attributeName = name.getValue(faceletContext);\n                        if (containsMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(attributeName, expression);\n                        } else {\n                            final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);\n                            parent.getAttributes().put(attributeName, literalValue);\n                        }\n                    }\n                } else {\n                    throw new FacesException(\"Type \" + mode + \" not supported\");\n                }\n            } else {\n\n                final String nameValue = name.getValue(faceletContext);\n                if (Attributes.RENDERED.equals(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.setRendered(value.getBoolean(faceletContext));\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));\n                    }\n                } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)\n                        && parent instanceof SupportsRenderedPartially) {\n\n                    if (value.isLiteral()) {\n                        final String[] components = ComponentUtils.splitList(value.getValue());\n                        ((SupportsRenderedPartially) parent).setRenderedPartially(components);\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                } else if (Attributes.STYLE_CLASS.equals(nameValue)) {\n                    // TODO expression\n                    ComponentUtils.setStyleClasses(parent, value.getValue());\n                } else if (Attributes.MARKUP.equals(nameValue)) {\n                    if (parent instanceof SupportsMarkup) {\n                        if (value.isLiteral()) {\n                            ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));\n                        } else {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(nameValue, expression);\n                        }\n                    } else {\n                        LOG.error(\"Component is not instanceof SupportsMarkup. Instance is: \" + parent.getClass().getName());\n                    }\n                } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {\n                    final MethodExpression methodExpression\n                            = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));\n                    }\n                } else if (parent instanceof EditableValueHolder\n                        && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {\n                    final MethodExpression methodExpression =\n                            getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValueChangeListener(\n                                new MethodExpressionValueChangeListener(methodExpression));\n                    }\n                } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {\n                    setConverter(faceletContext, parent, nameValue);\n                } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {\n                    final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);\n                    if (action != null) {\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {\n                    final MethodExpression action\n                            = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);\n                    if (action != null) {\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));\n                    }\n                } else if (!parent.getAttributes().containsKey(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.getAttributes().put(nameValue, value.getValue());\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                }\n            }\n        }\n    }\n\n    private boolean isMethodOrValueExpression(final String string) {\n        return (string.startsWith(\"${\") || string.startsWith(\"#{\")) && string.endsWith(\"}\");\n    }\n\n    private boolean containsMethodOrValueExpression(final String string) {\n        return (string.contains(\"${\") || string.contains(\"#{\")) && string.contains(\"}\");\n    }\n\n    private boolean isSimpleExpression(final String string) {\n        return string.indexOf('.') < 0 && string.indexOf('[') < 0;\n    }\n\n    private String removeElParenthesis(final String string) {\n        return string.substring(2, string.length() - 1);\n    }\n\n    private ValueExpression getExpression(final FaceletContext faceletContext) {\n        final String myValue = removeElParenthesis(value.getValue());\n        return faceletContext.getVariableMapper().resolveVariable(myValue);\n    }\n\n    private MethodExpression getMethodExpression(\n            final FaceletContext faceletContext, final Class returnType, final Class[] args) {\n        // in a composition may be we get the method expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,\n                        expression.getExpressionString(), returnType, args));\n            } else {\n                return null;\n            }\n        } else {\n            return value.getMethodExpression(faceletContext, returnType, args);\n        }\n    }\n\n    private Object getValue(\n            final FaceletContext faceletContext, final UIComponent parent, final String expressionString,\n            final String attributeName) {\n        Class type = Object.class;\n        try {\n            type = PropertyUtils.getReadMethod(\n                    new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();\n        } catch (final IntrospectionException e) {\n            LOG.warn(\"Can't determine expected type\", e);\n        }\n        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n        final ValueExpression valueExpression = expressionFactory\n                .createValueExpression(faceletContext, expressionString, type);\n        return valueExpression.getValue(faceletContext);\n    }\n\n    private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {\n        // in a composition may be we get the converter expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                setConverter(faceletContext, parent, nameValue, expression);\n            }\n        } else {\n            setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));\n        }\n    }\n\n    private void setConverter(\n            final FaceletContext faceletContext, final UIComponent parent, final String nameValue,\n            final ValueExpression expression) {\n        if (expression.isLiteralText()) {\n            final Converter converter =\n                    faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());\n            ((ValueHolder) parent).setConverter(converter);\n        } else {\n            parent.setValueExpression(nameValue, expression);\n        }\n    }\n\n    public static void letsAddASimpleMethod() {\n        System.out.println(\"Howdy!\");\n    }\n}"
  },
  {
    "path": "cost-benefit-calculator/src/test/resources/org/apache/myfaces/tobago/facelets/AttributeHandlerAndSorter.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.myfaces.tobago.facelets;\n\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.apache.myfaces.tobago.component.Attributes;\nimport org.apache.myfaces.tobago.component.SupportsMarkup;\nimport org.apache.myfaces.tobago.component.SupportsRenderedPartially;\nimport org.apache.myfaces.tobago.context.Markup;\nimport org.apache.myfaces.tobago.el.ConstantMethodBinding;\nimport org.apache.myfaces.tobago.internal.util.StringUtils;\nimport org.apache.myfaces.tobago.util.ComponentUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ELException;\nimport javax.el.ExpressionFactory;\nimport javax.el.MethodExpression;\nimport javax.el.ValueExpression;\nimport javax.faces.FacesException;\nimport javax.faces.component.ActionSource;\nimport javax.faces.component.ActionSource2;\nimport javax.faces.component.EditableValueHolder;\nimport javax.faces.component.UIComponent;\nimport javax.faces.component.ValueHolder;\nimport javax.faces.convert.Converter;\nimport javax.faces.event.MethodExpressionActionListener;\nimport javax.faces.event.MethodExpressionValueChangeListener;\nimport javax.faces.validator.MethodExpressionValidator;\nimport javax.faces.view.facelets.ComponentHandler;\nimport javax.faces.view.facelets.FaceletContext;\nimport javax.faces.view.facelets.TagAttribute;\nimport javax.faces.view.facelets.TagConfig;\nimport javax.faces.view.facelets.TagException;\nimport javax.faces.view.facelets.TagHandler;\nimport java.beans.IntrospectionException;\nimport java.beans.PropertyDescriptor;\n\nimport org.apache.myfaces.tobago.event.SortActionEvent;\nimport org.apache.myfaces.tobago.internal.component.AbstractUICommand;\nimport org.apache.myfaces.tobago.internal.component.AbstractUISheet;\nimport org.apache.myfaces.tobago.internal.util.StringUtils;\nimport org.apache.myfaces.tobago.model.SheetState;\nimport org.apache.myfaces.tobago.util.BeanComparator;\nimport org.apache.myfaces.tobago.util.ValueExpressionComparator;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ValueExpression;\nimport javax.faces.component.UIColumn;\nimport javax.faces.component.UICommand;\nimport javax.faces.component.UIComponent;\nimport javax.faces.component.UIInput;\nimport javax.faces.component.UIOutput;\nimport javax.faces.component.UISelectBoolean;\nimport javax.faces.component.UISelectMany;\nimport javax.faces.component.UISelectOne;\nimport javax.faces.context.FacesContext;\nimport javax.faces.model.DataModel;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\npublic final class AttributeHandlerAndSorter extends TagHandler {\n\n    private static final Logger LOG = LoggerFactory.getLogger(org.apache.myfaces.tobago.facelets.AttributeHandler.class);\n\n    private final TagAttribute name;\n\n    private final TagAttribute value;\n\n    private final TagAttribute mode;\n\n    public AttributeHandler(final TagConfig config) {\n        super(config);\n        this.name = getRequiredAttribute(Attributes.NAME);\n        this.value = getRequiredAttribute(Attributes.VALUE);\n        this.mode = getAttribute(Attributes.MODE);\n    }\n\n    public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {\n        if (parent == null) {\n            throw new TagException(tag, \"Parent UIComponent was null\");\n        }\n\n        if (ComponentHandler.isNew(parent)) {\n\n            if (mode != null) {\n                if (\"isNotSet\".equals(mode.getValue())) {\n                    boolean result = false;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = true;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = false;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"isSet\".equals(mode.getValue())) {\n                    boolean result = true;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = false;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = true;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isNotEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"action\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                // when the action hasn't been set while using a composition.\n                                if (LOG.isDebugEnabled()) {\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (\"actionListener\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                if (LOG.isDebugEnabled()) {\n                                    // when the action hasn't been set while using a composition.\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            LOG.warn(\"Only expressions are supported mode=actionListener value='\" + expressionString + \"'\");\n                            expressionString = null;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression actionListener\n                                = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));\n                    }\n                } else if (\"actionFromValue\".equals(mode.getValue())) {\n                    if (!value.isLiteral()) {\n                        final String result = value.getValue(faceletContext);\n                        parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));\n                    }\n                } else if (\"valueIfSet\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    String lastExpressionString = null;\n                    while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {\n                        final ValueExpression expression\n                                = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                        if (expression != null) {\n                            lastExpressionString = expressionString;\n                            expressionString = expression.getExpressionString();\n                        } else {\n                            // restore last value\n                            expressionString = lastExpressionString;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final String attributeName = name.getValue(faceletContext);\n                        if (containsMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(attributeName, expression);\n                        } else {\n                            final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);\n                            parent.getAttributes().put(attributeName, literalValue);\n                        }\n                    }\n                } else {\n                    throw new FacesException(\"Type \" + mode + \" not supported\");\n                }\n            } else {\n\n                final String nameValue = name.getValue(faceletContext);\n                if (Attributes.RENDERED.equals(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.setRendered(value.getBoolean(faceletContext));\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));\n                    }\n                } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)\n                        && parent instanceof SupportsRenderedPartially) {\n\n                    if (value.isLiteral()) {\n                        final String[] components = ComponentUtils.splitList(value.getValue());\n                        ((SupportsRenderedPartially) parent).setRenderedPartially(components);\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                } else if (Attributes.STYLE_CLASS.equals(nameValue)) {\n                    // TODO expression\n                    ComponentUtils.setStyleClasses(parent, value.getValue());\n                } else if (Attributes.MARKUP.equals(nameValue)) {\n                    if (parent instanceof SupportsMarkup) {\n                        if (value.isLiteral()) {\n                            ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));\n                        } else {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(nameValue, expression);\n                        }\n                    } else {\n                        LOG.error(\"Component is not instanceof SupportsMarkup. Instance is: \" + parent.getClass().getName());\n                    }\n                } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {\n                    final MethodExpression methodExpression\n                            = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));\n                    }\n                } else if (parent instanceof EditableValueHolder\n                        && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {\n                    final MethodExpression methodExpression =\n                            getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValueChangeListener(\n                                new MethodExpressionValueChangeListener(methodExpression));\n                    }\n                } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {\n                    setConverter(faceletContext, parent, nameValue);\n                } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {\n                    final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);\n                    if (action != null) {\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {\n                    final MethodExpression action\n                            = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);\n                    if (action != null) {\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));\n                    }\n                } else if (!parent.getAttributes().containsKey(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.getAttributes().put(nameValue, value.getValue());\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                }\n            }\n        }\n    }\n\n    private boolean isMethodOrValueExpression(final String string) {\n        return (string.startsWith(\"${\") || string.startsWith(\"#{\")) && string.endsWith(\"}\");\n    }\n\n    private boolean containsMethodOrValueExpression(final String string) {\n        return (string.contains(\"${\") || string.contains(\"#{\")) && string.contains(\"}\");\n    }\n\n    private boolean isSimpleExpression(final String string) {\n        return string.indexOf('.') < 0 && string.indexOf('[') < 0;\n    }\n\n    private String removeElParenthesis(final String string) {\n        return string.substring(2, string.length() - 1);\n    }\n\n    private ValueExpression getExpression(final FaceletContext faceletContext) {\n        final String myValue = removeElParenthesis(value.getValue());\n        return faceletContext.getVariableMapper().resolveVariable(myValue);\n    }\n\n    private MethodExpression getMethodExpression(\n            final FaceletContext faceletContext, final Class returnType, final Class[] args) {\n        // in a composition may be we get the method expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,\n                        expression.getExpressionString(), returnType, args));\n            } else {\n                return null;\n            }\n        } else {\n            return value.getMethodExpression(faceletContext, returnType, args);\n        }\n    }\n\n    private Object getValue(\n            final FaceletContext faceletContext, final UIComponent parent, final String expressionString,\n            final String attributeName) {\n        Class type = Object.class;\n        try {\n            type = PropertyUtils.getReadMethod(\n                    new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();\n        } catch (final IntrospectionException e) {\n            LOG.warn(\"Can't determine expected type\", e);\n        }\n        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n        final ValueExpression valueExpression = expressionFactory\n                .createValueExpression(faceletContext, expressionString, type);\n        return valueExpression.getValue(faceletContext);\n    }\n\n    private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {\n        // in a composition may be we get the converter expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                setConverter(faceletContext, parent, nameValue, expression);\n            }\n        } else {\n            setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));\n        }\n    }\n\n    private void setConverter(\n            final FaceletContext faceletContext, final UIComponent parent, final String nameValue,\n            final ValueExpression expression) {\n        if (expression.isLiteralText()) {\n            final Converter converter =\n                    faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());\n            ((ValueHolder) parent).setConverter(converter);\n        } else {\n            parent.setValueExpression(nameValue, expression);\n        }\n    }\n}\n\n//http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/component/Sorter.java/?v=source\nclass Sorter {\n\n    private static final Logger LOG = LoggerFactory.getLogger(Sorter.class);\n\n    private Comparator comparator;\n\n    /**\n     * @deprecated Please use {@link #perform(org.apache.myfaces.tobago.internal.component.AbstractUISheet)}\n     */\n    @Deprecated\n    public void perform(final SortActionEvent sortEvent) {\n        final AbstractUISheet data = (AbstractUISheet) sortEvent.getComponent();\n        perform(data);\n    }\n\n    public void perform(final AbstractUISheet data) {\n\n        Object value = data.getValue();\n        if (value instanceof DataModel) {\n            value = ((DataModel) value).getWrappedData();\n        }\n        final FacesContext facesContext = FacesContext.getCurrentInstance();\n        final SheetState sheetState = data.getSheetState(facesContext);\n\n        final String sortedColumnId = sheetState.getSortedColumnId();\n        if (LOG.isDebugEnabled()) {\n            LOG.debug(\"sorterId = '{}'\", sortedColumnId);\n        }\n\n        if (sortedColumnId == null) {\n            // not to be sorted\n            return;\n        }\n\n        final UIColumn column = (UIColumn) data.findComponent(sortedColumnId);\n        if (column == null) {\n            LOG.warn(\"No column to sort found, sorterId = '{}'\", sortedColumnId);\n            return;\n        }\n\n        final Comparator actualComparator;\n\n        if (value instanceof List || value instanceof Object[]) {\n            final String sortProperty;\n\n            try {\n                final UIComponent child = getFirstSortableChild(column.getChildren());\n                if (child != null) {\n\n                    final String attributeName = child instanceof AbstractUICommand ? Attributes.LABEL : Attributes.VALUE;\n                    if (child.getValueExpression(attributeName) != null) {\n                        final String var = data.getVar();\n                        if (var == null) {\n                            LOG.error(\"No sorting performed. Property var of sheet is not set!\");\n                            unsetSortableAttribute(column);\n                            return;\n                        }\n                        String expressionString = child.getValueExpression(attributeName).getExpressionString();\n                        if (isSimpleProperty(expressionString)) {\n                            if (expressionString.startsWith(\"#{\")\n                                    && expressionString.endsWith(\"}\")) {\n                                expressionString =\n                                        expressionString.substring(2,\n                                                expressionString.length() - 1);\n                            }\n                            sortProperty = expressionString.substring(var.length() + 1);\n\n                            actualComparator = new BeanComparator(\n                                    sortProperty, comparator, !sheetState.isAscending());\n\n                            if (LOG.isDebugEnabled()) {\n                                LOG.debug(\"Sort property is {}\", sortProperty);\n                            }\n                        } else {\n\n                            final boolean descending = !sheetState.isAscending();\n                            final ValueExpression expression = child.getValueExpression(\"value\");\n                            actualComparator = new ValueExpressionComparator(facesContext, var, expression, descending, comparator);\n                        }\n                    } else {\n                        LOG.error(\"No sorting performed. No Expression target found for sorting!\");\n                        unsetSortableAttribute(column);\n                        return;\n                    }\n                } else {\n                    LOG.error(\"No sorting performed. Value is not instanceof List or Object[]!\");\n                    unsetSortableAttribute(column);\n                    return;\n                }\n            } catch (final Exception e) {\n                LOG.error(\"Error while extracting sortMethod :\" + e.getMessage(), e);\n                if (column != null) {\n                    unsetSortableAttribute(column);\n                }\n                return;\n            }\n\n            // TODO: locale / comparator parameter?\n            // don't compare numbers with Collator.getInstance() comparator\n//        Comparator comparator = Collator.getInstance();\n//          comparator = new RowComparator(ascending, method);\n\n            // memorize selected rows\n            List<Object> selectedDataRows = null;\n            if (sheetState.getSelectedRows().size() > 0) {\n                selectedDataRows = new ArrayList<Object>(sheetState.getSelectedRows().size());\n                Object dataRow;\n                for (final Integer index : sheetState.getSelectedRows()) {\n                    if (value instanceof List) {\n                        dataRow = ((List) value).get(index);\n                    } else {\n                        dataRow = ((Object[]) value)[index];\n                    }\n                    selectedDataRows.add(dataRow);\n                }\n            }\n\n            // do sorting\n            if (value instanceof List) {\n                Collections.sort((List) value, actualComparator);\n            } else { // value is instanceof Object[]\n                Arrays.sort((Object[]) value, actualComparator);\n            }\n\n            // restore selected rows\n            if (selectedDataRows != null) {\n                sheetState.getSelectedRows().clear();\n                for (final Object dataRow : selectedDataRows) {\n                    int index = -1;\n                    if (value instanceof List) {\n                        for (int i = 0; i < ((List) value).size() && index < 0; i++) {\n                            if (dataRow == ((List) value).get(i)) {\n                                index = i;\n                            }\n                        }\n                    } else {\n                        for (int i = 0; i < ((Object[]) value).length && index < 0; i++) {\n                            if (dataRow == ((Object[]) value)[i]) {\n                                index = i;\n                            }\n                        }\n                    }\n                    if (index >= 0) {\n                        sheetState.getSelectedRows().add(index);\n                    }\n                }\n            }\n\n        } else {  // DataModel?, ResultSet, Result or Object\n            LOG.warn(\"Sorting not supported for type \"\n                    + (value != null ? value.getClass().toString() : \"null\"));\n        }\n    }\n\n    // XXX needs to be tested\n    // XXX was based on ^#\\{(\\w+(\\.\\w)*)\\}$ which is wrong, because there is a + missing after the last \\w\n    boolean isSimpleProperty(final String expressionString) {\n        if (expressionString.startsWith(\"#{\") && expressionString.endsWith(\"}\")) {\n            final String inner = expressionString.substring(2, expressionString.length() - 1);\n            final String[] parts = StringUtils.split(inner, '.');\n            for (final String part : parts) {\n                if (!StringUtils.isAlpha(part)) {\n                    return false;\n                }\n            }\n            return true;\n        }\n        return false;\n    }\n\n    private void unsetSortableAttribute(final UIColumn uiColumn) {\n        LOG.warn(\"removing attribute sortable from column \" + uiColumn.getId());\n        uiColumn.getAttributes().put(Attributes.SORTABLE, Boolean.FALSE);\n    }\n\n    private UIComponent getFirstSortableChild(final List<UIComponent> children) {\n        UIComponent result = null;\n\n        for (UIComponent child : children) {\n            result = child;\n            if (child instanceof UISelectMany\n                    || child instanceof UISelectOne\n                    || child instanceof UISelectBoolean\n                    || (child instanceof AbstractUICommand && child.getChildren().isEmpty())\n                    || (child instanceof UIInput && RendererTypes.HIDDEN.equals(child.getRendererType()))) {\n                continue;\n                // look for a better component if any\n            }\n            if (child instanceof UIOutput) {\n                break;\n            }\n            if (child instanceof UICommand\n                    || child instanceof javax.faces.component.UIPanel) {\n                child = getFirstSortableChild(child.getChildren());\n                if (child instanceof UIOutput) {\n                    break;\n                }\n            }\n        }\n        return result;\n    }\n\n    public Comparator getComparator() {\n        return comparator;\n    }\n\n    public void setComparator(final Comparator comparator) {\n        this.comparator = comparator;\n    }\n}\n"
  },
  {
    "path": "coverage/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <!-- From https://cylab.be/blog/97/compute-code-coverage-for-a-multi-module-maven-project-with-jacoco -->\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <artifactId>coverage</artifactId>\n\n    <description>Compute aggregated test code coverage</description>\n\n    <properties>\n        <maven.deploy.skip>true</maven.deploy.skip>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.hjug.refactorfirst.changepronenessranker</groupId>\n            <artifactId>change-proneness-ranker</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.effortranker</groupId>\n            <artifactId>effort-ranker</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.dsm</groupId>\n            <artifactId>graph-algorithms</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.costbenefitcalculator</groupId>\n            <artifactId>cost-benefit-calculator</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.graphdatagenerator</groupId>\n            <artifactId>graph-data-generator</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.plugin</groupId>\n            <artifactId>refactor-first-maven-plugin</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.report</groupId>\n            <artifactId>report</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.jacoco</groupId>\n                <artifactId>jacoco-maven-plugin</artifactId>\n                <version>0.8.8</version>\n                <executions>\n                    <execution>\n                        <id>report-aggregate</id>\n                        <phase>verify</phase>\n                        <goals>\n                            <goal>report-aggregate</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "effort-ranker/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.effortranker</groupId>\n    <artifactId>effort-ranker</artifactId>\n\n    <name>RefactorFirst Effort Ranker</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>net.sourceforge.pmd</groupId>\n            <artifactId>pmd-java</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.testresources</groupId>\n            <artifactId>test-resources</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n\n    </dependencies>\n\n    \n</project>"
  },
  {
    "path": "effort-ranker/src/main/java/org/hjug/metrics/CBOClass.java",
    "content": "package org.hjug.metrics;\n\nimport java.util.Scanner;\nimport lombok.Data;\n\n/**\n * Created by Jim on 11/16/2016.\n */\n@Data\npublic class CBOClass implements Disharmony {\n\n    private String className;\n    private String fileName;\n    private String packageName;\n\n    private Integer couplingCount;\n\n    public CBOClass(String className, String fileName, String packageName, String result) {\n        this.className = className;\n        this.fileName = fileName;\n        this.packageName = packageName;\n\n        try (Scanner scanner = new Scanner(result)) {\n            couplingCount = scanner.useDelimiter(\"[^\\\\d]+\").nextInt();\n        }\n    }\n}\n"
  },
  {
    "path": "effort-ranker/src/main/java/org/hjug/metrics/Disharmony.java",
    "content": "package org.hjug.metrics;\n\npublic interface Disharmony {\n\n    String getFileName();\n\n    String getClassName();\n\n    String getPackageName();\n}\n"
  },
  {
    "path": "effort-ranker/src/main/java/org/hjug/metrics/GodClass.java",
    "content": "package org.hjug.metrics;\n\nimport java.text.NumberFormat;\nimport java.text.ParseException;\nimport lombok.Data;\n\n/**\n * Created by Jim on 11/16/2016.\n */\n@Data\npublic class GodClass implements Disharmony {\n\n    private String className;\n    private String fileName;\n    private String packageName;\n    private Integer wmc;\n    private Integer atfd;\n    private Float tcc;\n\n    private Integer wmcRank;\n    private Integer atfdRank;\n    private Integer tccRank;\n    private Integer sumOfRanks;\n    private Integer overallRank;\n\n    public GodClass(String className, String fileName, String packageName, String result) {\n        this.className = className;\n        this.fileName = fileName;\n        this.packageName = packageName;\n\n        NumberFormat integerFormat = NumberFormat.getIntegerInstance();\n\n        String[] values =\n                result.substring(result.indexOf(\"(\") + 1, result.indexOf(\")\")).split(\", \");\n        try {\n            wmc = (int) (long) integerFormat.parse(extractValue(values[0]));\n            atfd = (int) (long) integerFormat.parse(extractValue(values[1]));\n        } catch (ParseException e) {\n            throw new RuntimeException(e);\n        }\n        String rawTcc = extractValue(values[2]);\n        tcc = Float.valueOf(rawTcc.replace(\"%\", \"\"));\n    }\n\n    private String extractValue(String value) {\n        return value.split(\"=\")[1];\n    }\n}\n"
  },
  {
    "path": "effort-ranker/src/main/java/org/hjug/metrics/GodClassRanker.java",
    "content": "package org.hjug.metrics;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.function.Function;\nimport java.util.function.ObjIntConsumer;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Created by Wendy on 11/16/2016.\n */\n@Slf4j\npublic class GodClassRanker {\n\n    public void rankGodClasses(List<GodClass> godClasses) {\n        rankWmc(godClasses);\n        rankAtfd(godClasses);\n        rankTcc(godClasses);\n        computeOverallRank(godClasses);\n    }\n\n    void computeOverallRank(List<GodClass> godClasses) {\n\n        godClasses.forEach(godClass ->\n                godClass.setSumOfRanks(godClass.getWmcRank() + godClass.getAtfdRank() + godClass.getTccRank()));\n\n        godClasses.sort(Comparator.comparing(GodClass::getSumOfRanks));\n\n        Function<GodClass, Integer> getSumOfRanks = GodClass::getSumOfRanks;\n        ObjIntConsumer<GodClass> setOverallRank = GodClass::setOverallRank;\n\n        setRank(godClasses, getSumOfRanks, setOverallRank);\n    }\n\n    void rankWmc(List<GodClass> godClasses) {\n        log.info(\"Calculating Weighted Method per Class (WMC) Rank\");\n        godClasses.sort(Comparator.comparing(GodClass::getWmc));\n\n        Function<GodClass, Integer> getWmc = GodClass::getWmc;\n        ObjIntConsumer<GodClass> setWmcRank = GodClass::setWmcRank;\n\n        setRank(godClasses, getWmc, setWmcRank);\n    }\n\n    void rankAtfd(List<GodClass> godClasses) {\n        log.info(\"Calculating Access to Foreign Data (ATFD) Rank\");\n        godClasses.sort(Comparator.comparing(GodClass::getAtfd));\n\n        Function<GodClass, Integer> getAtfd = GodClass::getAtfd;\n        ObjIntConsumer<GodClass> setAtfdRank = GodClass::setAtfdRank;\n\n        setRank(godClasses, getAtfd, setAtfdRank);\n    }\n\n    void rankTcc(List<GodClass> godClasses) {\n        log.info(\"Calculating Tight Class Cohesion (TCC) Rank\");\n        godClasses.sort(Comparator.comparing(GodClass::getTcc));\n\n        Function<GodClass, Float> getTcc = GodClass::getTcc;\n        ObjIntConsumer<GodClass> setTccRank = GodClass::setTccRank;\n\n        setRank(godClasses, getTcc, setTccRank);\n    }\n\n    <T extends Number & Comparable<? super T>> void setRank(\n            List<GodClass> godClasses, Function<GodClass, T> getter, ObjIntConsumer<GodClass> setter) {\n        int rank = 1;\n        T previousValue = null;\n\n        for (GodClass godClass : godClasses) {\n            T value = getter.apply(godClass);\n            if (null == previousValue) {\n                previousValue = value;\n            }\n\n            if (value.compareTo(previousValue) > 0) {\n                setter.accept(godClass, ++rank);\n                previousValue = value;\n            } else {\n                setter.accept(godClass, rank);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "effort-ranker/src/main/java/org/hjug/metrics/rules/CBORule.java",
    "content": "package org.hjug.metrics.rules;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;\nimport net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;\nimport net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration;\nimport net.sourceforge.pmd.lang.java.ast.ASTFormalParameter;\nimport net.sourceforge.pmd.lang.java.ast.ASTLocalVariableDeclaration;\nimport net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;\nimport net.sourceforge.pmd.lang.java.ast.ASTType;\nimport net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;\nimport net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;\nimport net.sourceforge.pmd.lang.java.types.JTypeMirror;\nimport net.sourceforge.pmd.properties.NumericConstraints;\nimport net.sourceforge.pmd.properties.PropertyBuilder;\nimport net.sourceforge.pmd.properties.PropertyDescriptor;\nimport net.sourceforge.pmd.properties.PropertyFactory;\n\n/**\n * Copy of PMD's CouplingBetweenObjectsRule\n * but generates the originally intended message containing coupling count\n */\npublic class CBORule extends AbstractJavaRule {\n    private static final PropertyDescriptor<Integer> THRESHOLD_DESCRIPTOR = ((PropertyBuilder.GenericPropertyBuilder)\n                    ((PropertyBuilder.GenericPropertyBuilder)\n                                    ((PropertyBuilder.GenericPropertyBuilder) PropertyFactory.intProperty(\"threshold\")\n                                                    .desc(\"Unique type reporting threshold\"))\n                                            .require(NumericConstraints.positive()))\n                            .defaultValue(20))\n            .build();\n    private int couplingCount;\n    private boolean inInterface;\n    private final Set<JTypeMirror> typesFoundSoFar = new HashSet();\n\n    private String message;\n\n    public CBORule() {\n        this.definePropertyDescriptor(THRESHOLD_DESCRIPTOR);\n    }\n\n    @Override\n    public String getMessage() {\n        return message;\n    }\n\n    @Override\n    public Object visit(ASTCompilationUnit cu, Object data) {\n        super.visit(cu, data);\n        if (this.couplingCount > 20) { // (Integer) this.getProperty(THRESHOLD_DESCRIPTOR)) {\n            message = \"A value of \" + this.couplingCount + \" may denote a high amount of coupling within the class\";\n            this.addViolation(data, cu, message);\n            this.setMessage(message);\n        }\n\n        this.couplingCount = 0;\n        this.typesFoundSoFar.clear();\n        return null;\n    }\n\n    public Object visit(ASTClassOrInterfaceDeclaration node, Object data) {\n        boolean prev = this.inInterface;\n        this.inInterface = node.isInterface();\n        super.visit(node, data);\n        this.inInterface = prev;\n        return null;\n    }\n\n    public Object visit(ASTMethodDeclaration node, Object data) {\n        ASTType type = node.getResultTypeNode();\n        this.checkVariableType(type);\n        return super.visit(node, data);\n    }\n\n    public Object visit(ASTLocalVariableDeclaration node, Object data) {\n        ASTType type = node.getTypeNode();\n        this.checkVariableType(type);\n        return super.visit(node, data);\n    }\n\n    public Object visit(ASTFormalParameter node, Object data) {\n        ASTType type = node.getTypeNode();\n        this.checkVariableType(type);\n        return super.visit(node, data);\n    }\n\n    public Object visit(ASTFieldDeclaration node, Object data) {\n        ASTType type = node.getTypeNode();\n        this.checkVariableType(type);\n        return super.visit(node, data);\n    }\n\n    private void checkVariableType(ASTType typeNode) {\n        if (!this.inInterface && typeNode != null) {\n            JTypeMirror t = typeNode.getTypeMirror();\n            if (!this.ignoreType(typeNode, t) && this.typesFoundSoFar.add(t)) {\n                ++this.couplingCount;\n            }\n        }\n    }\n\n    private boolean ignoreType(ASTType typeNode, JTypeMirror t) {\n        if (typeNode.getEnclosingType() != null\n                && typeNode.getEnclosingType().getSymbol().equals(t.getSymbol())) {\n            return true;\n        } else {\n            JTypeDeclSymbol symbol = t.getSymbol();\n            return symbol == null\n                    || \"java.lang\".equals(symbol.getPackageName())\n                    || t.isPrimitive()\n                    || t.isBoxedPrimitive();\n        }\n    }\n}\n"
  },
  {
    "path": "effort-ranker/src/test/java/org/hjug/metrics/CBOClassParsingTest.java",
    "content": "package org.hjug.metrics;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.Locale;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\npublic class CBOClassParsingTest {\n\n    private Locale defaultLocale;\n\n    @BeforeEach\n    public void before() {\n        defaultLocale = Locale.getDefault(Locale.Category.FORMAT);\n        Locale.setDefault(Locale.Category.FORMAT, Locale.ENGLISH);\n    }\n\n    @AfterEach\n    public void after() {\n        Locale.setDefault(defaultLocale);\n    }\n\n    @Test\n    void test() {\n        String result = \"A value of 20 may denote a high amount of coupling within the class\";\n        CBOClass cboClass = new CBOClass(\"a\", \"a.txt\", \"org.hjug\", result);\n        assertEquals(Integer.valueOf(20), cboClass.getCouplingCount());\n    }\n}\n"
  },
  {
    "path": "effort-ranker/src/test/java/org/hjug/metrics/GodClassParsingTest.java",
    "content": "package org.hjug.metrics;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.Locale;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\npublic class GodClassParsingTest {\n\n    private Locale defaultLocale;\n\n    @BeforeEach\n    public void before() {\n        defaultLocale = Locale.getDefault(Locale.Category.FORMAT);\n        Locale.setDefault(Locale.Category.FORMAT, Locale.ENGLISH);\n    }\n\n    @AfterEach\n    public void after() {\n        Locale.setDefault(defaultLocale);\n    }\n\n    @Test\n    void test() {\n        String result = \"Possible God Class (WMC=9200, ATFD=1,700, TCC=4.597%)\";\n        GodClass god = new GodClass(\"a\", \"a.txt\", \"org.hjug\", result);\n        assertEquals(Integer.valueOf(9200), god.getWmc());\n        assertEquals(Integer.valueOf(1700), god.getAtfd());\n        assertEquals(Float.valueOf(4.597f), god.getTcc());\n    }\n}\n"
  },
  {
    "path": "effort-ranker/src/test/java/org/hjug/metrics/GodClassRankerTest.java",
    "content": "package org.hjug.metrics;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Created by Wendy on 11/16/2016.\n */\npublic class GodClassRankerTest {\n\n    private final GodClassRanker godClassRanker = new GodClassRanker();\n\n    private final GodClass attributeHandler = new GodClass(\n            \"AttributeHandler\",\n            \"org/hjug/git/AttributeHandler.java\",\n            \"org.apache.myfaces.tobago.facelets\",\n            \"null (WMC=79, ATFD=79, TCC=0.027777777777777776)\");\n    private final GodClass attributeHandler2 = new GodClass(\n            \"AttributeHandler\",\n            \"org/hjug/git/AttributeHandler.java\",\n            \"org.apache.myfaces.tobago.facelets\",\n            \"null (WMC=79, ATFD=79, TCC=0.027777777777777776)\");\n    private final GodClass sorter = new GodClass(\n            \"Sorter\", \"Sorter.java\", \"org.apache.myfaces.tobago.facelets\", \" God class (WMC=51, ATFD=25, TCC=0.2)\");\n    private final GodClass sorter2 = new GodClass(\n            \"Sorter\", \"Sorter2.java\", \"org.apache.myfaces.tobago.facelets\", \" God class (WMC=51, ATFD=25, TCC=0.2)\");\n    private final GodClass themeImpl = new GodClass(\n            \"ThemeImpl\",\n            \"ThemeImpl.java\",\n            \"org.apache.myfaces.tobago.facelets\",\n            \"God class (WMC=60, ATFD=16, TCC=0.07816091954022988)\");\n    private final GodClass themeImpl2 = new GodClass(\n            \"ThemeImpl\",\n            \"ThemeImpl2.java\",\n            \"org.apache.myfaces.tobago.facelets\",\n            \"God class (WMC=60, ATFD=16, TCC=0.07816091954022988)\");\n\n    private final List<GodClass> godClasses = new ArrayList<>();\n\n    @BeforeEach\n    public void setUp() {\n        godClasses.add(attributeHandler);\n        godClasses.add(sorter);\n        godClasses.add(themeImpl);\n    }\n\n    @Test\n    void testRankGodClasses() {\n        godClassRanker.rankGodClasses(godClasses);\n\n        Assertions.assertEquals(\"ThemeImpl.java\", godClasses.get(0).getFileName());\n        Assertions.assertEquals(\"Sorter.java\", godClasses.get(1).getFileName());\n        Assertions.assertEquals(\n                \"org/hjug/git/AttributeHandler.java\", godClasses.get(2).getFileName());\n\n        Assertions.assertEquals(5, godClasses.get(0).getSumOfRanks().longValue());\n        Assertions.assertEquals(6, godClasses.get(1).getSumOfRanks().longValue());\n        Assertions.assertEquals(7, godClasses.get(2).getSumOfRanks().longValue());\n\n        Assertions.assertEquals(1, godClasses.get(0).getOverallRank().longValue());\n        Assertions.assertEquals(2, godClasses.get(1).getOverallRank().longValue());\n        Assertions.assertEquals(3, godClasses.get(2).getOverallRank().longValue());\n    }\n\n    @Test\n    void testWmcRanker() {\n        godClassRanker.rankWmc(godClasses);\n\n        Assertions.assertEquals(\"Sorter.java\", godClasses.get(0).getFileName());\n        Assertions.assertEquals(\"ThemeImpl.java\", godClasses.get(1).getFileName());\n        Assertions.assertEquals(\n                \"org/hjug/git/AttributeHandler.java\", godClasses.get(2).getFileName());\n\n        Assertions.assertEquals(1, godClasses.get(0).getWmcRank().longValue());\n        Assertions.assertEquals(2, godClasses.get(1).getWmcRank().longValue());\n        Assertions.assertEquals(3, godClasses.get(2).getWmcRank().longValue());\n    }\n\n    @Test\n    void testWmcRankerWithDupeValue() {\n        godClasses.add(themeImpl2);\n        godClassRanker.rankWmc(godClasses);\n\n        Assertions.assertEquals(1, godClasses.get(0).getWmcRank().longValue());\n        Assertions.assertEquals(2, godClasses.get(1).getWmcRank().longValue());\n        Assertions.assertEquals(2, godClasses.get(2).getWmcRank().longValue());\n        Assertions.assertEquals(3, godClasses.get(3).getWmcRank().longValue());\n    }\n\n    @Test\n    void testAtfdRanker() {\n        godClassRanker.rankAtfd(godClasses);\n\n        Assertions.assertEquals(\"ThemeImpl.java\", godClasses.get(0).getFileName());\n        Assertions.assertEquals(\"Sorter.java\", godClasses.get(1).getFileName());\n        Assertions.assertEquals(\n                \"org/hjug/git/AttributeHandler.java\", godClasses.get(2).getFileName());\n\n        Assertions.assertEquals(1, godClasses.get(0).getAtfdRank().longValue());\n        Assertions.assertEquals(2, godClasses.get(1).getAtfdRank().longValue());\n        Assertions.assertEquals(3, godClasses.get(2).getAtfdRank().longValue());\n    }\n\n    @Test\n    void testAtfdRankerWithDupeValue() {\n        godClasses.add(sorter2);\n        godClassRanker.rankAtfd(godClasses);\n\n        Assertions.assertEquals(1, godClasses.get(0).getAtfdRank().longValue());\n        Assertions.assertEquals(2, godClasses.get(1).getAtfdRank().longValue());\n        Assertions.assertEquals(2, godClasses.get(2).getAtfdRank().longValue());\n        Assertions.assertEquals(3, godClasses.get(3).getAtfdRank().longValue());\n    }\n\n    @Test\n    void testTccRanker() {\n        godClassRanker.rankTcc(godClasses);\n\n        Assertions.assertEquals(\n                \"org/hjug/git/AttributeHandler.java\", godClasses.get(0).getFileName());\n        Assertions.assertEquals(\"ThemeImpl.java\", godClasses.get(1).getFileName());\n        Assertions.assertEquals(\"Sorter.java\", godClasses.get(2).getFileName());\n\n        Assertions.assertEquals(1, godClasses.get(0).getTccRank().longValue());\n        Assertions.assertEquals(2, godClasses.get(1).getTccRank().longValue());\n        Assertions.assertEquals(3, godClasses.get(2).getTccRank().longValue());\n    }\n\n    @Test\n    void testTccRankerWithDuplicateValue() {\n        godClasses.add(attributeHandler2);\n        godClassRanker.rankTcc(godClasses);\n\n        // Two classes with a rank of 1\n        Assertions.assertEquals(1, godClasses.get(0).getTccRank().longValue());\n        Assertions.assertEquals(1, godClasses.get(1).getTccRank().longValue());\n        Assertions.assertEquals(2, godClasses.get(2).getTccRank().longValue());\n        Assertions.assertEquals(3, godClasses.get(3).getTccRank().longValue());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.dsm</groupId>\n    <artifactId>graph-algorithms</artifactId>\n\n    <name>RefactorFirst Graph Algorithms</name>\n\n    <description>\n        Implementation of a DSM that only has JGraphT-Core as a dependency.\n        Can be used by other projects.\n    </description>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.jgrapht</groupId>\n            <artifactId>jgrapht-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jgrapht</groupId>\n            <artifactId>jgrapht-opt</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n            <version>33.4.8-jre</version>\n        </dependency>\n    </dependencies>\n    \n</project>"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/dsm/CircularReferenceChecker.java",
    "content": "package org.hjug.dsm;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.AsSubgraph;\n\n@Slf4j\npublic class CircularReferenceChecker<V, E> {\n\n    private final Map<V, AsSubgraph<V, E>> uniqueSubGraphs = new HashMap<>();\n\n    /**\n     * Detects cycles in the graph that is passed in\n     * and returns the unique cycles in the graph as a map of subgraphs\n     *\n     * @param graph\n     * @return a Map of unique cycles in the graph\n     */\n    public Map<V, AsSubgraph<V, E>> getCycles(Graph<V, E> graph) {\n\n        if (!uniqueSubGraphs.isEmpty()) {\n            return uniqueSubGraphs;\n        }\n\n        // use CycleDetector.findCycles()?\n        Map<V, AsSubgraph<V, E>> cycles = detectCycles(graph);\n\n        cycles.forEach((vertex, subGraph) -> {\n            int vertexCount = subGraph.vertexSet().size();\n            int edgeCount = subGraph.edgeSet().size();\n\n            if (vertexCount > 1 && edgeCount > 1 && !isDuplicateSubGraph(subGraph, vertex)) {\n                uniqueSubGraphs.put(vertex, subGraph);\n                log.debug(\"Vertex: {} vertex count: {} edge count: {}\", vertex, vertexCount, edgeCount);\n            }\n        });\n\n        return uniqueSubGraphs;\n    }\n\n    private boolean isDuplicateSubGraph(AsSubgraph<V, E> subGraph, V vertex) {\n        if (!uniqueSubGraphs.isEmpty()) {\n            for (AsSubgraph<V, E> renderedSubGraph : uniqueSubGraphs.values()) {\n                if (renderedSubGraph.vertexSet().size() == subGraph.vertexSet().size()\n                        && renderedSubGraph.edgeSet().size()\n                                == subGraph.edgeSet().size()\n                        && renderedSubGraph.vertexSet().contains(vertex)) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    private Map<V, AsSubgraph<V, E>> detectCycles(Graph<V, E> graph) {\n        Map<V, AsSubgraph<V, E>> cyclesForEveryVertexMap = new HashMap<>();\n        CycleDetector<V, E> cycleDetector = new CycleDetector<>(graph);\n        cycleDetector.findCycles().forEach(v -> {\n            AsSubgraph<V, E> subGraph = new AsSubgraph<>(graph, cycleDetector.findCyclesContainingVertex(v));\n            cyclesForEveryVertexMap.put(v, subGraph);\n        });\n        return cyclesForEveryVertexMap;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/dsm/DSM.java",
    "content": "package org.hjug.dsm;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport lombok.Getter;\nimport org.jgrapht.Graph;\nimport org.jgrapht.Graphs;\nimport org.jgrapht.alg.connectivity.KosarajuStrongConnectivityInspector;\nimport org.jgrapht.alg.util.Triple;\nimport org.jgrapht.opt.graph.sparse.SparseIntDirectedWeightedGraph;\n\n/*\nGenerated with Generative AI using a prompt similar to the following and iterated on:\nProvide a complete implementation of a Numerical DSM with integer weighted edges in Java.\nInclude as many comments as possible in the implementation to make it easy to understand.\nUse JGraphT classes and methods to the greatest extent possible.\nConstruction of the DSM should take place as follows.\nFirst, Place nodes with empty rows at the top of the DSM.\nSecond, Place nodes with empty columns on the right of the DSM.\nThird, identify strongly connected nodes and treat them as a single node using JGraphT's TarjanSimpleCycles class.\nFourth, order all edges in the DSM with a topological sort that permits cycles in the graph after identifying strongly connected components.\nFifth, Print the DSM in a method that performs no sorting or ordering - it should only print rows and columns.\nWhen the DSM is printed, label the columns and rows.\nPlace dashes on the diagonal when printing.\ninclude a method tht returns all edges above the diagonal.\ninclude another method that returns the optimal edge above the diagonal to remove,\ninclude a third method that identifies all minimum weight edges to remove above the diagonal.\n\nUsed https://sookocheff.com/post/dsm/improving-software-architecture-using-design-structure-matrix/#optimizing-processes\nas a starting point.\n */\n\npublic class DSM<V, E> {\n    private final Graph<V, E> graph;\n    private List<V> sortedActivities;\n    boolean activitiesSorted = false;\n    private final List<E> edgesAboveDiagonal = new ArrayList<>();\n\n    List<Integer> sparseIntSortedActivities;\n    SparseIntDirectedWeightedGraph sparseGraph;\n\n    @Getter\n    double sumOfEdgeWeightsAboveDiagonal;\n\n    Map<V, Integer> vertexToInt = new HashMap<>();\n    Map<Integer, V> intToVertex = new HashMap<>();\n    List<Triple<Integer, Integer, Double>> sparseEdges = new ArrayList<>();\n    int vertexCount = 0;\n\n    public DSM(Graph<V, E> graph) {\n        this.graph = graph;\n        sortedActivities = new ArrayList<>();\n    }\n\n    public void addActivity(V activity) {\n        graph.addVertex(activity);\n    }\n\n    public void addDependency(V from, V to, int weight) {\n        E edge = graph.addEdge(from, to);\n        if (edge != null) {\n            graph.setEdgeWeight(edge, weight);\n        }\n    }\n\n    private void orderVertices() {\n        sparseGraph = getSparseIntDirectedWeightedGraph();\n        List<Set<Integer>> sccs = this.findStronglyConnectedSparseGraphComponents(sparseGraph);\n        sparseIntSortedActivities = topologicalSortSparseGraph(sccs, sparseGraph);\n        // reversing corrects rendering of the DSM\n        // with sources as rows and targets as columns\n        // was needed after AI solution was generated and iterated\n        Collections.reverse(sparseIntSortedActivities);\n        sortedActivities = convertIntToStringVertices(sparseIntSortedActivities);\n        activitiesSorted = true;\n    }\n\n    private SparseIntDirectedWeightedGraph getSparseIntDirectedWeightedGraph() {\n        for (V vertex : graph.vertexSet()) {\n            vertexToInt.put(vertex, vertexCount);\n            intToVertex.put(vertexCount, vertex);\n            vertexCount++;\n        }\n\n        // Create the list of sparseEdges for the SparseIntDirectedWeightedGraph\n        for (E edge : graph.edgeSet()) {\n            int source = vertexToInt.get(graph.getEdgeSource(edge));\n            int target = vertexToInt.get(graph.getEdgeTarget(edge));\n            double weight = graph.getEdgeWeight(edge);\n            sparseEdges.add(Triple.of(source, target, weight));\n        }\n\n        // Create the SparseIntDirectedWeightedGraph\n        return new SparseIntDirectedWeightedGraph(vertexCount, sparseEdges);\n    }\n\n    List<V> convertIntToStringVertices(List<Integer> intVertices) {\n        return intVertices.stream().map(intToVertex::get).collect(Collectors.toList());\n    }\n\n    /**\n     * Kosaraju SCC detector avoids stack overflow.\n     * It is used by JGraphT's CycleDetector, and makes sense to use it here as well for consistency\n     *\n     * @param graph\n     * @return\n     */\n    private List<Set<Integer>> findStronglyConnectedSparseGraphComponents(Graph<Integer, Integer> graph) {\n        KosarajuStrongConnectivityInspector<Integer, Integer> kosaraju =\n                new KosarajuStrongConnectivityInspector<>(graph);\n        return kosaraju.stronglyConnectedSets();\n    }\n\n    private List<Integer> topologicalSortSparseGraph(List<Set<Integer>> sccs, Graph<Integer, Integer> graph) {\n        List<Integer> sortedActivities = new ArrayList<>();\n        Set<Integer> visited = new HashSet<>();\n\n        for (Set<Integer> scc : sccs) {\n            for (Integer activity : scc) {\n                if (!visited.contains(activity)) {\n                    topologicalSortUtilSparseGraph(activity, visited, sortedActivities, graph);\n                }\n            }\n        }\n\n        Collections.reverse(sortedActivities);\n        return sortedActivities;\n    }\n\n    private void topologicalSortUtilSparseGraph(\n            Integer activity, Set<Integer> visited, List<Integer> sortedActivities, Graph<Integer, Integer> graph) {\n        visited.add(activity);\n\n        for (Integer neighbor : Graphs.successorListOf(graph, activity)) {\n            if (!visited.contains(neighbor)) {\n                topologicalSortUtilSparseGraph(neighbor, visited, sortedActivities, graph);\n            }\n        }\n\n        sortedActivities.add(activity);\n    }\n\n    public List<E> getEdgesAboveDiagonal() {\n        if (!activitiesSorted) {\n            orderVertices();\n        }\n\n        if (edgesAboveDiagonal.isEmpty()) {\n            for (int i = 0; i < sortedActivities.size(); i++) {\n                for (int j = i + 1; j < sortedActivities.size(); j++) {\n                    // source / destination vertex was flipped after solution generation\n                    // to correctly identify the vertex above the diagonal to remove\n                    E edge = graph.getEdge(sortedActivities.get(i), sortedActivities.get(j));\n                    if (edge != null) {\n                        edgesAboveDiagonal.add(edge);\n                    }\n                }\n            }\n\n            sumOfEdgeWeightsAboveDiagonal = edgesAboveDiagonal.stream()\n                    .mapToInt(edge -> (int) graph.getEdgeWeight(edge))\n                    .sum();\n        }\n\n        return edgesAboveDiagonal;\n    }\n\n    private List<Integer> getSparseEdgesAboveDiagonal() {\n        if (!activitiesSorted) {\n            orderVertices();\n        }\n\n        List<Integer> sparseEdgesAboveDiagonal = new ArrayList<>();\n\n        for (int i = 0; i < sparseIntSortedActivities.size(); i++) {\n            for (int j = i + 1; j < sparseIntSortedActivities.size(); j++) {\n                // source / destination vertex was flipped after solution generation\n                // to correctly identify the vertex above the diagonal to remove\n                Integer edge = sparseGraph.getEdge(sparseIntSortedActivities.get(i), sparseIntSortedActivities.get(j));\n\n                if (edge != null) {\n                    sparseEdgesAboveDiagonal.add(edge);\n                }\n            }\n        }\n\n        return sparseEdgesAboveDiagonal;\n    }\n\n    public E getFirstLowestWeightEdgeAboveDiagonalToRemove() {\n        if (!activitiesSorted) {\n            orderVertices();\n        }\n\n        List<E> edgesAboveDiagonal = getEdgesAboveDiagonal();\n        E optimalEdge = null;\n        int minWeight = Integer.MAX_VALUE;\n\n        for (E edge : edgesAboveDiagonal) {\n            int weight = (int) graph.getEdgeWeight(edge);\n            if (weight < minWeight) {\n                minWeight = weight;\n                optimalEdge = edge;\n                if (minWeight == 1) {\n                    break;\n                }\n            }\n        }\n\n        return optimalEdge;\n    }\n\n    public List<E> getMinimumWeightEdgesAboveDiagonal() {\n        if (!activitiesSorted) {\n            orderVertices();\n        }\n\n        List<E> edgesAboveDiagonal = getEdgesAboveDiagonal();\n        List<E> minWeightEdges = new ArrayList<>();\n        double minWeight = Double.MAX_VALUE;\n\n        for (E edge : edgesAboveDiagonal) {\n            double weight = graph.getEdgeWeight(edge);\n            if (weight < minWeight) {\n                minWeight = weight;\n                minWeightEdges.clear();\n                minWeightEdges.add(edge);\n            } else if (weight == minWeight) {\n                minWeightEdges.add(edge);\n            }\n        }\n\n        return minWeightEdges;\n    }\n\n    public void printDSM() {\n        if (!activitiesSorted) {\n            orderVertices();\n        }\n\n        printDSM(graph, sortedActivities);\n    }\n\n    void printDSM(Graph<V, E> graph, List<V> sortedActivities) {\n\n        System.out.println(\"Design Structure Matrix:\");\n        System.out.print(\"  \");\n        for (V col : sortedActivities) {\n            System.out.print(col + \" \");\n        }\n        System.out.println();\n        for (V row : sortedActivities) {\n            System.out.print(row + \" \");\n            for (V col : sortedActivities) {\n                if (col.equals(row)) {\n                    System.out.print(\"- \");\n                } else {\n                    E edge = graph.getEdge(row, col);\n                    if (edge != null) {\n                        System.out.print((int) graph.getEdgeWeight(edge) + \" \");\n                    } else {\n                        System.out.print(\"0 \");\n                    }\n                }\n            }\n            System.out.println();\n        }\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/dsm/EdgeRemovalCalculator.java",
    "content": "package org.hjug.dsm;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.AsSubgraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.graph.SimpleDirectedWeightedGraph;\n\npublic class EdgeRemovalCalculator {\n\n    private final Graph<String, DefaultWeightedEdge> graph;\n    private DSM<String, DefaultWeightedEdge> dsm;\n    private final Map<String, AsSubgraph<String, DefaultWeightedEdge>> cycles;\n    private Set<DefaultWeightedEdge> edgesToRemove;\n\n    public EdgeRemovalCalculator(Graph<String, DefaultWeightedEdge> graph, DSM<String, DefaultWeightedEdge> dsm) {\n        this.graph = graph;\n        this.dsm = dsm;\n        this.cycles = new CircularReferenceChecker<String, DefaultWeightedEdge>().getCycles(graph);\n    }\n\n    public EdgeRemovalCalculator(Graph<String, DefaultWeightedEdge> graph, Set<DefaultWeightedEdge> edgesToRemove) {\n        this.graph = graph;\n        this.edgesToRemove = edgesToRemove;\n        this.cycles = new CircularReferenceChecker<String, DefaultWeightedEdge>().getCycles(graph);\n    }\n\n    /**\n     * Captures the impact of the removal of each edge above the diagonal.\n     */\n    public List<EdgeToRemoveInfo> getImpactOfEdgesAboveDiagonalIfRemoved(int limit) {\n        // get edges above diagonal for DSM graph\n        List<DefaultWeightedEdge> edgesAboveDiagonal;\n        List<DefaultWeightedEdge> allEdgesAboveDiagonal = dsm.getEdgesAboveDiagonal();\n\n        if (limit == 0 || allEdgesAboveDiagonal.size() <= limit) {\n            edgesAboveDiagonal = allEdgesAboveDiagonal;\n        } else {\n            // get first 50 values of min weight\n            List<DefaultWeightedEdge> minimumWeightEdgesAboveDiagonal = dsm.getMinimumWeightEdgesAboveDiagonal();\n            int max = Math.min(minimumWeightEdgesAboveDiagonal.size(), limit);\n            edgesAboveDiagonal = minimumWeightEdgesAboveDiagonal.subList(0, max);\n        }\n\n        int currentCycleCount = cycles.size();\n\n        return edgesAboveDiagonal.stream()\n                .map(this::calculateEdgeToRemoveInfo)\n                .sorted(\n                        Comparator.comparing((EdgeToRemoveInfo edgeToRemoveInfo) ->\n                                currentCycleCount - edgeToRemoveInfo.getNewCycleCount())\n                        /*.thenComparing(EdgeToRemoveInfo::getEdgeWeight)*/ )\n                .collect(Collectors.toList());\n    }\n\n    public List<EdgeToRemoveInfo> getImpactOfEdges() {\n        int currentCycleCount = cycles.size();\n\n        return edgesToRemove.stream()\n                .map(this::calculateEdgeToRemoveInfo)\n                .sorted(\n                        Comparator.comparing((EdgeToRemoveInfo edgeToRemoveInfo) ->\n                                currentCycleCount - edgeToRemoveInfo.getNewCycleCount())\n                        /*.thenComparing(EdgeToRemoveInfo::getEdgeWeight)*/ )\n                .collect(Collectors.toList());\n    }\n\n    public EdgeToRemoveInfo calculateEdgeToRemoveInfo(DefaultWeightedEdge edgeToRemove) {\n        // clone graph and remove edge\n        Graph<String, DefaultWeightedEdge> improvedGraph = new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        graph.vertexSet().forEach(improvedGraph::addVertex);\n        for (DefaultWeightedEdge weightedEdge : graph.edgeSet()) {\n            improvedGraph.addEdge(graph.getEdgeSource(weightedEdge), graph.getEdgeTarget(weightedEdge), weightedEdge);\n        }\n\n        improvedGraph.removeEdge(edgeToRemove);\n\n        // Calculate new cycle count\n        int newCycleCount = new CircularReferenceChecker<String, DefaultWeightedEdge>()\n                .getCycles(improvedGraph)\n                .size();\n\n        // calculate new graph statistics\n        double removedEdgeWeight = graph.getEdgeWeight(edgeToRemove);\n        double payoff = newCycleCount / removedEdgeWeight;\n        return new EdgeToRemoveInfo(edgeToRemove, (int) removedEdgeWeight, newCycleCount, payoff);\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/dsm/EdgeToRemoveInfo.java",
    "content": "package org.hjug.dsm;\n\nimport lombok.Data;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n@Data\npublic class EdgeToRemoveInfo {\n    private final DefaultWeightedEdge edge;\n    private final int removedEdgeWeight;\n    private final int newCycleCount;\n    private final double payoff; // impact / effort\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/dsm/OptimalBackEdgeRemover.java",
    "content": "package org.hjug.dsm;\n\nimport java.util.*;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.alg.cycle.JohnsonSimpleCycles;\nimport org.jgrapht.graph.AsSubgraph;\n\npublic class OptimalBackEdgeRemover<V, E> {\n    private Graph<V, E> graph;\n\n    /**\n     * Constructor initializing with the target graph.\n     * @param graph The directed weighted graph to analyze\n     */\n    public OptimalBackEdgeRemover(Graph<V, E> graph) {\n        this.graph = graph;\n    }\n\n    /**\n     * Finds the optimal back edge(s) to remove to move the graph closer to a DAG.\n     * @return A set of edges to remove\n     */\n    public Set<E> findOptimalBackEdgesToRemove() {\n        CycleDetector<V, E> cycleDetector = new CycleDetector<>(graph);\n\n        // If the graph is already acyclic, return empty set\n        if (!cycleDetector.detectCycles()) {\n            return Collections.emptySet();\n        }\n\n        // Find all cycles in the graph\n        JohnsonSimpleCycles<V, E> cycleFinder = new JohnsonSimpleCycles<>(graph);\n        List<List<V>> originalCycles = cycleFinder.findSimpleCycles();\n        int originalCycleCount = originalCycles.size();\n\n        // Identify edges that are part of at least one cycle\n        Set<E> edgesInCycles = new HashSet<>();\n        for (List<V> cycle : originalCycles) {\n            for (int i = 0; i < cycle.size(); i++) {\n                V source = cycle.get(i);\n                V target = cycle.get((i + 1) % cycle.size());\n                E edge = graph.getEdge(source, target);\n                edgesInCycles.add(edge);\n            }\n        }\n\n        // Calculate cycle elimination count for each edge\n        Map<E, Integer> edgeCycleEliminationCount = new HashMap<>();\n        for (E edge : edgesInCycles) {\n            // Create a subgraph without this edge\n            Graph<V, E> subgraph = new AsSubgraph<>(graph, graph.vertexSet(), new HashSet<>(graph.edgeSet()));\n            subgraph.removeEdge(edge);\n\n            // Calculate how many cycles would be eliminated\n            JohnsonSimpleCycles<V, E> subgraphCycleFinder = new JohnsonSimpleCycles<>(subgraph);\n            List<List<V>> remainingCycles = subgraphCycleFinder.findSimpleCycles();\n            int cyclesEliminated = originalCycleCount - remainingCycles.size();\n\n            edgeCycleEliminationCount.put(edge, cyclesEliminated);\n        }\n\n        // Find edges that eliminate the most cycles\n        int maxCycleElimination = 0;\n        List<E> maxEliminationEdges = new ArrayList<>();\n\n        for (Map.Entry<E, Integer> entry : edgeCycleEliminationCount.entrySet()) {\n            if (entry.getValue() > maxCycleElimination) {\n                maxCycleElimination = entry.getValue();\n                maxEliminationEdges.clear();\n                maxEliminationEdges.add(entry.getKey());\n            } else if (entry.getValue() == maxCycleElimination) {\n                maxEliminationEdges.add(entry.getKey());\n            }\n        }\n\n        // If no cycles are eliminated (shouldn't happen), return empty set\n        if (maxEliminationEdges.isEmpty() || maxCycleElimination == 0) {\n            return Collections.emptySet();\n        }\n\n        // If multiple edges eliminate the same number of cycles, choose the one with the lowest weight\n        if (maxEliminationEdges.size() > 1) {\n            double minWeight = Double.MAX_VALUE;\n            List<E> minWeightEdges = new ArrayList<>();\n\n            for (E edge : maxEliminationEdges) {\n                double weight = graph.getEdgeWeight(edge);\n                if (weight < minWeight) {\n                    minWeight = weight;\n                    minWeightEdges.clear();\n                    minWeightEdges.add(edge);\n                } else if (weight == minWeight) {\n                    minWeightEdges.add(edge);\n                }\n            }\n\n            return new HashSet<>(minWeightEdges);\n        }\n\n        // Return the single edge that eliminates the most cycles\n        return new HashSet<>(maxEliminationEdges);\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/dsm/SparseGraphCircularReferenceChecker.java",
    "content": "package org.hjug.dsm;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.AsSubgraph;\nimport org.jgrapht.opt.graph.sparse.SparseIntDirectedWeightedGraph;\n\n@Slf4j\npublic class SparseGraphCircularReferenceChecker {\n\n    private final Map<Integer, AsSubgraph<Integer, Integer>> uniqueSubGraphs = new HashMap<>();\n\n    /**\n     * Detects cycles in the graph that is passed in\n     * and returns the unique cycles in the graph as a map of subgraphs\n     *\n     * @param graph\n     * @return a Map of unique cycles in the graph\n     */\n    public Map<Integer, AsSubgraph<Integer, Integer>> getCycles(SparseIntDirectedWeightedGraph graph) {\n\n        if (!uniqueSubGraphs.isEmpty()) {\n            return uniqueSubGraphs;\n        }\n\n        // use CycleDetector.findCycles()?\n        Map<Integer, AsSubgraph<Integer, Integer>> cycles = detectCycles(graph);\n\n        cycles.forEach((vertex, subGraph) -> {\n            int vertexCount = subGraph.vertexSet().size();\n            int edgeCount = subGraph.edgeSet().size();\n\n            if (vertexCount > 1 && edgeCount > 1 && !isDuplicateSubGraph(subGraph, vertex)) {\n                uniqueSubGraphs.put(vertex, subGraph);\n                log.debug(\"Vertex: {} vertex count: {} edge count: {}\", vertex, vertexCount, edgeCount);\n            }\n        });\n\n        return uniqueSubGraphs;\n    }\n\n    private boolean isDuplicateSubGraph(AsSubgraph<Integer, Integer> subGraph, Integer vertex) {\n        if (!uniqueSubGraphs.isEmpty()) {\n            for (AsSubgraph<Integer, Integer> renderedSubGraph : uniqueSubGraphs.values()) {\n                if (renderedSubGraph.vertexSet().size() == subGraph.vertexSet().size()\n                        && renderedSubGraph.edgeSet().size()\n                                == subGraph.edgeSet().size()\n                        && renderedSubGraph.vertexSet().contains(vertex)) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    private Map<Integer, AsSubgraph<Integer, Integer>> detectCycles(SparseIntDirectedWeightedGraph graph) {\n        Map<Integer, AsSubgraph<Integer, Integer>> cyclesForEveryVertexMap = new HashMap<>();\n        CycleDetector<Integer, Integer> cycleDetector = new CycleDetector<>(graph);\n        cycleDetector.findCycles().forEach(v -> {\n            AsSubgraph<Integer, Integer> subGraph =\n                    new AsSubgraph<>(graph, cycleDetector.findCyclesContainingVertex(v));\n            cyclesForEveryVertexMap.put(v, subGraph);\n        });\n        return cyclesForEveryVertexMap;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/dsm/SparseIntDWGEdgeRemovalCalculator.java",
    "content": "package org.hjug.dsm;\n\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.concurrent.ConcurrentSkipListSet;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport org.jgrapht.Graph;\nimport org.jgrapht.Graphs;\nimport org.jgrapht.alg.connectivity.KosarajuStrongConnectivityInspector;\nimport org.jgrapht.alg.util.Triple;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.opt.graph.sparse.SparseIntDirectedWeightedGraph;\n\nclass SparseIntDWGEdgeRemovalCalculator {\n    private final Graph<String, DefaultWeightedEdge> graph;\n    SparseIntDirectedWeightedGraph sparseGraph;\n    List<Triple<Integer, Integer, Double>> sparseEdges;\n    List<Integer> sparseEdgesAboveDiagonal;\n    private final double sumOfEdgeWeightsAboveDiagonal;\n    int vertexCount;\n    Map<String, Integer> vertexToInt;\n    Map<Integer, String> intToVertex;\n\n    SparseIntDWGEdgeRemovalCalculator(\n            Graph<String, DefaultWeightedEdge> graph,\n            SparseIntDirectedWeightedGraph sparseGraph,\n            List<Triple<Integer, Integer, Double>> sparseEdges,\n            List<Integer> sparseEdgesAboveDiagonal,\n            double sumOfEdgeWeightsAboveDiagonal,\n            int vertexCount,\n            Map<String, Integer> vertexToInt,\n            Map<Integer, String> intToVertex) {\n        this.graph = graph;\n        this.sparseGraph = sparseGraph;\n        this.sparseEdges = new CopyOnWriteArrayList<>(sparseEdges);\n        this.sparseEdgesAboveDiagonal = new CopyOnWriteArrayList<>(sparseEdgesAboveDiagonal);\n        this.sumOfEdgeWeightsAboveDiagonal = sumOfEdgeWeightsAboveDiagonal;\n        this.vertexCount = vertexCount;\n        this.vertexToInt = new ConcurrentHashMap<>(vertexToInt);\n        this.intToVertex = new ConcurrentHashMap<>(intToVertex);\n    }\n\n    public List<EdgeToRemoveInfo> getImpactOfSparseEdgesAboveDiagonalIfRemoved() {\n        return sparseEdgesAboveDiagonal.parallelStream()\n                .map(this::calculateSparseEdgeToRemoveInfo)\n                .sorted(Comparator.comparing(EdgeToRemoveInfo::getPayoff)\n                        .thenComparing(EdgeToRemoveInfo::getRemovedEdgeWeight))\n                .collect(Collectors.toList());\n    }\n\n    private EdgeToRemoveInfo calculateSparseEdgeToRemoveInfo(Integer edgeToRemove) {\n        // clone graph and remove edge\n        int source = sparseGraph.getEdgeSource(edgeToRemove);\n        int target = sparseGraph.getEdgeTarget(edgeToRemove);\n        double weight = sparseGraph.getEdgeWeight(edgeToRemove);\n        Triple<Integer, Integer, Double> removedEdge = Triple.of(source, target, weight);\n\n        List<Triple<Integer, Integer, Double>> tempUpdatedEdgeList = new ArrayList<>(sparseEdges);\n        tempUpdatedEdgeList.remove(removedEdge);\n        List<Triple<Integer, Integer, Double>> updatedEdgeList = new CopyOnWriteArrayList<>(tempUpdatedEdgeList);\n\n        SparseIntDirectedWeightedGraph improvedGraph = new SparseIntDirectedWeightedGraph(vertexCount, updatedEdgeList);\n\n        // find edges above diagonal\n        List<Integer> sortedSparseVertices = orderVertices(improvedGraph);\n        List<Integer> updatedEdges = getSparseEdgesAboveDiagonal(improvedGraph, sortedSparseVertices);\n\n        // calculate new graph statistics\n        int newEdgeCount = updatedEdges.size();\n        double newEdgeWeightSum =\n                updatedEdges.stream().mapToDouble(improvedGraph::getEdgeWeight).sum();\n        DefaultWeightedEdge defaultWeightedEdge = graph.getEdge(intToVertex.get(source), intToVertex.get(target));\n        double payoff = (sumOfEdgeWeightsAboveDiagonal - newEdgeWeightSum) / weight;\n        return new EdgeToRemoveInfo(defaultWeightedEdge, (int) weight, newEdgeCount, payoff);\n    }\n\n    private List<Integer> orderVertices(SparseIntDirectedWeightedGraph sparseGraph) {\n        List<Set<Integer>> sccs = new CopyOnWriteArrayList<>(findStronglyConnectedSparseGraphComponents(sparseGraph));\n        //        List<Integer> sparseIntSortedActivities = topologicalSortSparseGraph(sccs, sparseGraph);\n        List<Integer> sparseIntSortedActivities = topologicalParallelSortSparseGraph(sccs, sparseGraph);\n        // reversing corrects rendering of the DSM\n        // with sources as rows and targets as columns\n        // was needed after AI solution was generated and iterated\n        Collections.reverse(sparseIntSortedActivities);\n\n        return new CopyOnWriteArrayList<>(sparseIntSortedActivities);\n    }\n\n    /**\n     * Kosaraju SCC detector avoids stack overflow.\n     * It is used by JGraphT's CycleDetector, and makes sense to use it here as well for consistency\n     *\n     * @param graph\n     * @return\n     */\n    private List<Set<Integer>> findStronglyConnectedSparseGraphComponents(Graph<Integer, Integer> graph) {\n        KosarajuStrongConnectivityInspector<Integer, Integer> kosaraju =\n                new KosarajuStrongConnectivityInspector<>(graph);\n        return kosaraju.stronglyConnectedSets();\n    }\n\n    private List<Integer> topologicalSortSparseGraph(List<Set<Integer>> sccs, Graph<Integer, Integer> graph) {\n        List<Integer> sortedActivities = new ArrayList<>();\n        Set<Integer> visited = new HashSet<>();\n\n        sccs.parallelStream()\n                .flatMap(Set::parallelStream)\n                .filter(activity -> !visited.contains(activity))\n                .forEach(activity -> topologicalSortUtilSparseGraph(activity, visited, sortedActivities, graph));\n\n        Collections.reverse(sortedActivities);\n        return sortedActivities;\n    }\n\n    private void topologicalSortUtilSparseGraph(\n            Integer activity, Set<Integer> visited, List<Integer> sortedActivities, Graph<Integer, Integer> graph) {\n        visited.add(activity);\n\n        for (Integer neighbor : Graphs.successorListOf(graph, activity)) {\n            if (!visited.contains(neighbor)) {\n                topologicalSortUtilSparseGraph(neighbor, visited, sortedActivities, graph);\n            }\n        }\n\n        sortedActivities.add(activity);\n    }\n\n    private List<Integer> getSparseEdgesAboveDiagonal(\n            SparseIntDirectedWeightedGraph sparseGraph, List<Integer> sortedActivities) {\n        ConcurrentLinkedQueue<Integer> sparseEdgesAboveDiagonal = new ConcurrentLinkedQueue<>();\n\n        int size = sortedActivities.size();\n        IntStream.range(0, size).parallel().forEach(i -> {\n            for (int j = i + 1; j < size; j++) {\n                Integer edge = sparseGraph.getEdge(sortedActivities.get(i), sortedActivities.get(j));\n                if (edge != null) {\n                    sparseEdgesAboveDiagonal.add(edge);\n                }\n            }\n        });\n\n        return new ArrayList<>(sparseEdgesAboveDiagonal);\n    }\n\n    private List<Integer> topologicalParallelSortSparseGraph(List<Set<Integer>> sccs, Graph<Integer, Integer> graph) {\n        ConcurrentLinkedQueue<Integer> sortedActivities = new ConcurrentLinkedQueue<>();\n        Set<Integer> visited = new ConcurrentSkipListSet<>();\n\n        sccs.parallelStream()\n                .flatMap(Set::parallelStream)\n                .filter(activity -> !visited.contains(activity))\n                .forEach(activity -> topologicalSortUtilSparseGraph(activity, visited, sortedActivities, graph));\n\n        ArrayList<Integer> sortedActivitiesList = new ArrayList<>(sortedActivities);\n        Collections.reverse(sortedActivitiesList);\n        return sortedActivitiesList;\n    }\n\n    private void topologicalSortUtilSparseGraph(\n            Integer activity,\n            Set<Integer> visited,\n            ConcurrentLinkedQueue<Integer> sortedActivities,\n            Graph<Integer, Integer> graph) {\n        visited.add(activity);\n\n        Graphs.successorListOf(graph, activity).parallelStream()\n                .filter(neighbor -> !visited.contains(neighbor))\n                .forEach(neighbor -> topologicalSortUtilSparseGraph(neighbor, visited, sortedActivities, graph));\n\n        sortedActivities.add(activity);\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/SuperTypeToken.java",
    "content": "package org.hjug.feedback;\n\nimport java.lang.reflect.*;\n\npublic abstract class SuperTypeToken<T> {\n    private final Type type;\n\n    protected SuperTypeToken() {\n        Type superclass = getClass().getGenericSuperclass();\n        if (superclass instanceof ParameterizedType) {\n            this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];\n        } else {\n            throw new RuntimeException(\"Missing type parameter.\");\n        }\n    }\n\n    public Type getType() {\n        return type;\n    }\n\n    public Class<T> getClassFromTypeToken() {\n        return (Class<T>) getClassFromTypeToken(type);\n    }\n\n    // ((ParameterizedType) type).getActualTypeArguments()[0] - returns String in List<String>\n    static Class<?> getClassFromTypeToken(Type type) {\n        if (type instanceof Class<?>) {\n            return (Class<?>) type;\n        } else if (type instanceof ParameterizedType) {\n            return (Class<?>) ((ParameterizedType) type).getRawType();\n        } else if (type instanceof GenericArrayType) {\n            Type componentType = ((GenericArrayType) type).getGenericComponentType();\n            return java.lang.reflect.Array.newInstance(getClassFromTypeToken(componentType), 0)\n                    .getClass();\n        } else if (type instanceof TypeVariable<?>) {\n            // Type variables don't have a direct class representation\n            return Object.class; // Fallback\n        } else if (type instanceof WildcardType) {\n            Type[] upperBounds = ((WildcardType) type).getUpperBounds();\n            return getClassFromTypeToken(upperBounds[0]); // Use the first upper bound\n        }\n        throw new IllegalArgumentException(\"Unsupported Type: \" + type);\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/arc/EdgeInfo.java",
    "content": "package org.hjug.feedback.arc;\n\nimport lombok.Data;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n@Data\npublic class EdgeInfo {\n\n    private final DefaultWeightedEdge edge;\n    private final int presentInCycleCount;\n    private final boolean removeSource;\n    private final boolean removeTarget;\n    private final int weight;\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/arc/EdgeInfoCalculator.java",
    "content": "package org.hjug.feedback.arc;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport lombok.RequiredArgsConstructor;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.AsSubgraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n@RequiredArgsConstructor\npublic class EdgeInfoCalculator {\n\n    private final Graph<String, DefaultWeightedEdge> graph;\n    private final Collection<DefaultWeightedEdge> edgesToRemove;\n    private final Set<String> vertexesToRemove;\n    private final Map<String, AsSubgraph<String, DefaultWeightedEdge>> cycles;\n\n    public Collection<EdgeInfo> calculateEdgeInformation() {\n        List<EdgeInfo> edgeInfos = new ArrayList<>();\n\n        for (DefaultWeightedEdge edge : edgesToRemove) {\n            int presentInCycleCount = (int) cycles.values().stream()\n                    .filter(cycle -> cycle.containsEdge(edge))\n                    .count();\n\n            EdgeInfo edgeInfo = new EdgeInfo(\n                    edge,\n                    presentInCycleCount,\n                    vertexesToRemove.contains(graph.getEdgeSource(edge)),\n                    vertexesToRemove.contains(graph.getEdgeTarget(edge)),\n                    (int) graph.getEdgeWeight(edge));\n            edgeInfos.add(edgeInfo);\n        }\n\n        return edgeInfos.stream()\n                .sorted(Comparator.comparing(EdgeInfo::getPresentInCycleCount)\n                        .reversed()\n                        .thenComparing(edgeInfo -> edgeInfo.isRemoveSource() ? 0 : 1)\n                        .thenComparing(edgeInfo -> edgeInfo.isRemoveTarget() ? 0 : 1)\n                        .thenComparing(EdgeInfo::getWeight))\n                .collect(Collectors.toList());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/arc/approximate/FeedbackArcSetResult.java",
    "content": "package org.hjug.feedback.arc.approximate;\n\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * Result container for the Feedback Arc Set algorithm\n */\npublic class FeedbackArcSetResult<V, E> {\n    private final List<V> vertexSequence;\n    private final Set<E> feedbackArcs;\n\n    public FeedbackArcSetResult(List<V> vertexSequence, Set<E> feedbackArcs) {\n        this.vertexSequence = vertexSequence;\n        this.feedbackArcs = feedbackArcs;\n    }\n\n    public List<V> getVertexSequence() {\n        return vertexSequence;\n    }\n\n    public Set<E> getFeedbackArcs() {\n        return feedbackArcs;\n    }\n\n    public int getFeedbackArcCount() {\n        return feedbackArcs.size();\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\n                \"FeedbackArcSetResult{vertexSequence=%s, feedbackArcCount=%d}\", vertexSequence, feedbackArcs.size());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/arc/approximate/FeedbackArcSetSolver.java",
    "content": "package org.hjug.feedback.arc.approximate;\n\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\nimport org.jgrapht.Graph;\n\n/**\n * Parallel implementation of Algorithm GR for the Feedback Arc Set problem\n * Based on Eades, Lin, and Smyth's fast and effective heuristic\n * DOI: https://doi.org/10.1016/0020-0190(93)90079-O\n * https://researchportal.murdoch.edu.au/esploro/outputs/journalArticle/A-fast-and-effective-heuristic-for/991005543112107891\n * Generated by Perplexity.ai's Research model\n */\npublic class FeedbackArcSetSolver<V, E> {\n\n    private final Graph<V, E> graph;\n    private final ConcurrentHashMap<V, AtomicInteger> inDegreeMap;\n    private final ConcurrentHashMap<V, AtomicInteger> outDegreeMap;\n    private final ConcurrentHashMap<Integer, CopyOnWriteArrayList<V>> vertexBins;\n\n    public FeedbackArcSetSolver(Graph<V, E> graph) {\n        this.graph = graph;\n        this.inDegreeMap = new ConcurrentHashMap<>();\n        this.outDegreeMap = new ConcurrentHashMap<>();\n        this.vertexBins = new ConcurrentHashMap<>();\n        initializeDegrees();\n    }\n\n    /**\n     * Initialize degree maps using parallel streams for better performance\n     */\n    private void initializeDegrees() {\n        graph.vertexSet().parallelStream().forEach(vertex -> {\n            int inDegree = graph.inDegreeOf(vertex);\n            int outDegree = graph.outDegreeOf(vertex);\n\n            inDegreeMap.put(vertex, new AtomicInteger(inDegree));\n            outDegreeMap.put(vertex, new AtomicInteger(outDegree));\n\n            // Calculate delta value for bin sorting\n            int delta = outDegree - inDegree;\n            vertexBins.computeIfAbsent(delta, k -> new CopyOnWriteArrayList<>()).add(vertex);\n        });\n    }\n\n    /**\n     * Executes Algorithm GR to find a feedback arc set\n     * @return FeedbackArcSetResult containing the vertex sequence and feedback arcs\n     */\n    public FeedbackArcSetResult<V, E> solve() {\n        List<V> s1 = new CopyOnWriteArrayList<>(); // Left sequence\n        List<V> s2 = new CopyOnWriteArrayList<>(); // Right sequence\n        Set<V> remainingVertices = ConcurrentHashMap.newKeySet();\n        remainingVertices.addAll(graph.vertexSet());\n\n        Set<E> feedbackArcs = ConcurrentHashMap.newKeySet();\n\n        while (!remainingVertices.isEmpty()) {\n            // Process sinks in parallel\n            List<V> sinks = findSinks(remainingVertices);\n            sinks.parallelStream().forEach(sink -> {\n                s2.add(0, sink);\n                removeVertex(sink, remainingVertices, feedbackArcs);\n            });\n\n            if (remainingVertices.isEmpty()) break;\n\n            // Process sources in parallel\n            List<V> sources = findSources(remainingVertices);\n            sources.parallelStream().forEach(source -> {\n                s1.add(source);\n                removeVertex(source, remainingVertices, feedbackArcs);\n            });\n\n            if (remainingVertices.isEmpty()) break;\n\n            // Find vertex with maximum delta value\n            Optional<V> maxDeltaVertex = findMaxDeltaVertex(remainingVertices);\n            if (maxDeltaVertex.isPresent()) {\n                V vertex = maxDeltaVertex.get();\n                s1.add(vertex);\n                removeVertex(vertex, remainingVertices, feedbackArcs);\n            }\n        }\n\n        // Combine sequences\n        List<V> finalSequence = new ArrayList<>(s1);\n        finalSequence.addAll(s2);\n\n        // Calculate feedback arcs based on final sequence\n        Set<E> finalFeedbackArcs = calculateFeedbackArcs(finalSequence);\n\n        return new FeedbackArcSetResult<>(finalSequence, finalFeedbackArcs);\n    }\n\n    /**\n     * Find all sink vertices (vertices with out-degree 0) using parallel processing\n     */\n    private List<V> findSinks(Set<V> vertices) {\n        return vertices.parallelStream()\n                .filter(v -> outDegreeMap.get(v).get() == 0)\n                .collect(Collectors.toList());\n    }\n\n    /**\n     * Find all source vertices (vertices with in-degree 0) using parallel processing\n     */\n    private List<V> findSources(Set<V> vertices) {\n        return vertices.parallelStream()\n                .filter(v -> inDegreeMap.get(v).get() == 0)\n                .collect(Collectors.toList());\n    }\n\n    /**\n     * Find vertex with maximum delta value (out-degree - in-degree)\n     */\n    private Optional<V> findMaxDeltaVertex(Set<V> vertices) {\n        return vertices.parallelStream()\n                .max(Comparator.comparingInt(\n                        v -> outDegreeMap.get(v).get() - inDegreeMap.get(v).get()));\n    }\n\n    /**\n     * Remove vertex and update degrees of adjacent vertices\n     */\n    private void removeVertex(V vertex, Set<V> remainingVertices, Set<E> feedbackArcs) {\n        remainingVertices.remove(vertex);\n\n        // Update degrees of adjacent vertices in parallel\n        graph.incomingEdgesOf(vertex).parallelStream().forEach(edge -> {\n            V source = graph.getEdgeSource(edge);\n            if (remainingVertices.contains(source)) {\n                outDegreeMap.get(source).decrementAndGet();\n            }\n        });\n\n        graph.outgoingEdgesOf(vertex).parallelStream().forEach(edge -> {\n            V target = graph.getEdgeTarget(edge);\n            if (remainingVertices.contains(target)) {\n                inDegreeMap.get(target).decrementAndGet();\n            }\n        });\n    }\n\n    /**\n     * Calculate feedback arcs based on the final vertex sequence\n     */\n    private Set<E> calculateFeedbackArcs(List<V> sequence) {\n        Map<V, Integer> vertexPosition = new HashMap<>();\n        for (int i = 0; i < sequence.size(); i++) {\n            vertexPosition.put(sequence.get(i), i);\n        }\n\n        return graph.edgeSet().parallelStream()\n                .filter(edge -> {\n                    V source = graph.getEdgeSource(edge);\n                    V target = graph.getEdgeTarget(edge);\n                    return vertexPosition.get(source) > vertexPosition.get(target);\n                })\n                .collect(Collectors.toSet());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/arc/exact/FeedbackArcSetResult.java",
    "content": "package org.hjug.feedback.arc.exact;\n\nimport java.util.Set;\n\n/**\n * Result container for the minimum feedback arc set algorithm [2]\n */\npublic class FeedbackArcSetResult<V, E> {\n    private final Set<E> feedbackArcSet;\n    private final double objectiveValue;\n\n    public FeedbackArcSetResult(Set<E> feedbackArcSet, double objectiveValue) {\n        this.feedbackArcSet = feedbackArcSet;\n        this.objectiveValue = objectiveValue;\n    }\n\n    public Set<E> getFeedbackArcSet() {\n        return feedbackArcSet;\n    }\n\n    public double getObjectiveValue() {\n        return objectiveValue;\n    }\n\n    public int size() {\n        return feedbackArcSet.size();\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\n                \"FeedbackArcSetResult{arcSet=%s, objective=%.2f, size=%d}\",\n                feedbackArcSet, objectiveValue, feedbackArcSet.size());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/arc/exact/MinimumFeedbackArcSetSolver.java",
    "content": "package org.hjug.feedback.arc.exact;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.connectivity.KosarajuStrongConnectivityInspector;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.DefaultDirectedGraph;\n\n/**\n * Exact minimum feedback arc set solver using lazy constraint generation\n * Based on Baharev et al. \"An Exact Method for the Minimum Feedback Arc Set Problem\"\n * https://dl.acm.org/doi/10.1145/3446429\n * https://doi.org/10.1145/3446429\n * Generated by Perplexity.ai's Research model\n */\npublic class MinimumFeedbackArcSetSolver<V, E> {\n    private final Graph<V, E> graph;\n    private final Map<E, Double> edgeWeights;\n    private final Class<E> edgeClass;\n    private final ConcurrentHashMap<Set<E>, Boolean> cycleMatrix;\n    private final int maxIterations;\n\n    public MinimumFeedbackArcSetSolver(Graph<V, E> graph, Map<E, Double> edgeWeights, SuperTypeToken<E> edgeTypeToken) {\n        this.graph = graph;\n        this.edgeWeights = edgeWeights != null ? edgeWeights : createUniformWeights();\n        this.cycleMatrix = new ConcurrentHashMap<>();\n        this.maxIterations = 1000;\n        this.edgeClass = edgeTypeToken.getClassFromTypeToken();\n    }\n\n    /**\n     * Creates uniform weights for all edges when no weights are provided [2]\n     */\n    private Map<E, Double> createUniformWeights() {\n        Map<E, Double> weights = new ConcurrentHashMap<>();\n        graph.edgeSet().parallelStream().forEach(edge -> weights.put(edge, 1.0));\n        return weights;\n    }\n\n    /**\n     * Main solving method implementing the lazy constraint generation algorithm [2]\n     */\n    public FeedbackArcSetResult<V, E> solve() {\n        Set<E> bestFeedbackArcSet = ConcurrentHashMap.newKeySet();\n        double bestObjectiveValue;\n\n        // Initialize with a heuristic solution [2]\n        Set<E> initialSolution = computeInitialHeuristicSolution();\n        bestFeedbackArcSet.addAll(initialSolution);\n        bestObjectiveValue = calculateObjectiveValue(initialSolution);\n\n        AtomicInteger iteration = new AtomicInteger(0);\n        AtomicBoolean optimalityProved = new AtomicBoolean(false);\n\n        while (iteration.get() < maxIterations && !optimalityProved.get()) {\n            // Solve relaxed problem with current cycle matrix [2]\n            Set<E> relaxedSolution = solveRelaxedProblem();\n\n            // Check if solution is acyclic [12][16]\n            if (isAcyclic(createGraphWithoutEdges(relaxedSolution))) {\n                // Found optimal solution\n                double objectiveValue = calculateObjectiveValue(relaxedSolution);\n                if (objectiveValue < bestObjectiveValue) {\n                    bestFeedbackArcSet.clear();\n                    bestFeedbackArcSet.addAll(relaxedSolution);\n                    bestObjectiveValue = objectiveValue;\n                }\n                optimalityProved.set(true);\n                break;\n            }\n\n            // Find cycles and extend cycle matrix [2]\n            Set<List<E>> newCycles = findCyclesInSolution(relaxedSolution);\n            if (newCycles.isEmpty()) {\n                break; // No more cycles found\n            }\n\n            // Add new cycles to matrix using parallel processing [18]\n            newCycles.parallelStream().forEach(cycle -> {\n                Set<E> cycleEdges = new HashSet<>(cycle);\n                cycleMatrix.put(cycleEdges, Boolean.TRUE);\n            });\n\n            iteration.incrementAndGet();\n        }\n\n        return new FeedbackArcSetResult<>(bestFeedbackArcSet, bestObjectiveValue);\n    }\n\n    /**\n     * Computes initial heuristic solution using greedy approach [2]\n     */\n    private Set<E> computeInitialHeuristicSolution() {\n        Set<E> feedbackArcs = ConcurrentHashMap.newKeySet();\n        Graph<V, E> tempGraph = createGraphCopy();\n\n        // Use parallel processing to identify cycles [18]\n        while (hasCycles(tempGraph)) {\n            // Find strongly connected components [17][21]\n            KosarajuStrongConnectivityInspector<V, E> inspector = new KosarajuStrongConnectivityInspector<>(tempGraph);\n            List<Set<V>> sccs = inspector.stronglyConnectedSets();\n\n            // Process non-trivial SCCs in parallel [18]\n            Optional<E> edgeToRemove = sccs.parallelStream()\n                    .filter(scc -> scc.size() > 1)\n                    .flatMap(scc -> getEdgesInSCC(tempGraph, scc).stream())\n                    .min(Comparator.comparingDouble(edge -> edgeWeights.getOrDefault(edge, 1.0)));\n\n            if (edgeToRemove.isPresent()) {\n                E edge = edgeToRemove.get();\n                feedbackArcs.add(edge);\n                tempGraph.removeEdge(edge);\n            } else {\n                break;\n            }\n        }\n\n        return feedbackArcs;\n    }\n\n    /**\n     * Solves the relaxed integer programming problem [2]\n     */\n    private Set<E> solveRelaxedProblem() {\n        // Simplified relaxed problem solver\n        // In practice, this would use an integer programming solver\n        Set<E> solution = ConcurrentHashMap.newKeySet();\n\n        // Use greedy approach based on current cycle matrix [2]\n        Map<E, Long> edgeCycleCounts = new ConcurrentHashMap<>();\n\n        // Count how many cycles each edge participates in [18]\n        cycleMatrix.keySet().parallelStream()\n                .forEach(cycle -> cycle.forEach(edge -> edgeCycleCounts.merge(edge, 1L, Long::sum)));\n\n        // Select edges with highest cycle participation [2]\n        while (!cycleMatrix.isEmpty() && !isAllCyclesCovered(solution)) {\n            Optional<E> bestEdge = edgeCycleCounts.entrySet().parallelStream()\n                    .filter(entry -> !solution.contains(entry.getKey()))\n                    .max(Map.Entry.<E, Long>comparingByValue()\n                            .thenComparing(entry -> 1.0 / edgeWeights.getOrDefault(entry.getKey(), 1.0)))\n                    .map(Map.Entry::getKey);\n\n            if (bestEdge.isPresent()) {\n                solution.add(bestEdge.get());\n            } else {\n                break;\n            }\n        }\n\n        return solution;\n    }\n\n    /**\n     * Finds cycles in the current solution using breadth-first search [2][27]\n     */\n    private Set<List<E>> findCyclesInSolution(Set<E> solution) {\n        Set<List<E>> cycles = ConcurrentHashMap.newKeySet();\n        Graph<V, E> remainingGraph = createGraphWithoutEdges(solution);\n\n        // Use parallel processing to find cycles [18]\n        solution.parallelStream().forEach(edge -> {\n            V source = graph.getEdgeSource(edge);\n            V target = graph.getEdgeTarget(edge);\n\n            // Find path from target back to source in remaining graph [27]\n            List<E> pathBackToSource = findShortestPath(remainingGraph, target, source);\n            if (!pathBackToSource.isEmpty()) {\n                List<E> cycle = new ArrayList<>(pathBackToSource);\n                cycle.add(edge);\n                cycles.add(cycle);\n            }\n        });\n\n        return cycles;\n    }\n\n    /**\n     * Finds shortest path using breadth-first search [27]\n     */\n    private List<E> findShortestPath(Graph<V, E> graph, V start, V target) {\n        if (!graph.containsVertex(start) || !graph.containsVertex(target)) {\n            return List.of();\n        }\n\n        Queue<V> queue = new ConcurrentLinkedQueue<>();\n        Map<V, E> predecessorEdge = new ConcurrentHashMap<>();\n        Set<V> visited = ConcurrentHashMap.newKeySet();\n\n        queue.offer(start);\n        visited.add(start);\n\n        while (!queue.isEmpty()) {\n            V current = queue.poll();\n\n            if (current.equals(target)) {\n                // Reconstruct path [27]\n                List<E> path = new ArrayList<>();\n                V node = target;\n                while (predecessorEdge.containsKey(node)) {\n                    E edge = predecessorEdge.get(node);\n                    path.add(0, edge);\n                    node = graph.getEdgeSource(edge);\n                }\n                return path;\n            }\n\n            // Explore neighbors using parallel processing [18]\n            graph.outgoingEdgesOf(current).parallelStream()\n                    .map(graph::getEdgeTarget)\n                    .filter(neighbor -> !visited.contains(neighbor))\n                    .forEach(neighbor -> {\n                        if (visited.add(neighbor)) {\n                            predecessorEdge.put(neighbor, graph.getEdge(current, neighbor));\n                            queue.offer(neighbor);\n                        }\n                    });\n        }\n\n        return List.of();\n    }\n\n    /**\n     * Checks if graph is acyclic using cycle detector [12][16]\n     */\n    private boolean isAcyclic(Graph<V, E> graph) {\n        CycleDetector<V, E> detector = new CycleDetector<>(graph);\n        return !detector.detectCycles();\n    }\n\n    /**\n     * Checks if graph has cycles [12][16]\n     */\n    private boolean hasCycles(Graph<V, E> graph) {\n        CycleDetector<V, E> detector = new CycleDetector<>(graph);\n        return detector.detectCycles();\n    }\n\n    /**\n     * Creates a copy of the graph without specified edges [11]\n     */\n    private Graph<V, E> createGraphWithoutEdges(Set<E> excludedEdges) {\n        Graph<V, E> newGraph = new DefaultDirectedGraph<>(edgeClass);\n\n        // Add all vertices [11]\n        graph.vertexSet().forEach(newGraph::addVertex);\n\n        // Add edges not in excluded set [18]\n        graph.edgeSet().stream().filter(edge -> !excludedEdges.contains(edge)).forEach(edge -> {\n            V source = graph.getEdgeSource(edge);\n            V target = graph.getEdgeTarget(edge);\n            newGraph.addEdge(source, target);\n        });\n\n        return newGraph;\n    }\n\n    /**\n     * Creates a complete copy of the graph [11]\n     */\n    private Graph<V, E> createGraphCopy() {\n        Graph<V, E> copy = new DefaultDirectedGraph<>(edgeClass);\n\n        // Copy vertices and edges [11]\n        graph.vertexSet().forEach(copy::addVertex);\n        graph.edgeSet().forEach(edge -> {\n            V source = graph.getEdgeSource(edge);\n            V target = graph.getEdgeTarget(edge);\n            copy.addEdge(source, target);\n        });\n\n        return copy;\n    }\n\n    /**\n     * Gets edges within a strongly connected component [17]\n     */\n    private Set<E> getEdgesInSCC(Graph<V, E> graph, Set<V> scc) {\n        return graph.edgeSet().parallelStream()\n                .filter(edge -> {\n                    V source = graph.getEdgeSource(edge);\n                    V target = graph.getEdgeTarget(edge);\n                    return scc.contains(source) && scc.contains(target);\n                })\n                .collect(Collectors.toSet());\n    }\n\n    /**\n     * Checks if all cycles in the matrix are covered by the solution [2]\n     */\n    private boolean isAllCyclesCovered(Set<E> solution) {\n        return cycleMatrix.keySet().parallelStream()\n                .allMatch(cycle -> cycle.stream().anyMatch(solution::contains));\n    }\n\n    /**\n     * Calculates objective value for a solution [2]\n     */\n    private double calculateObjectiveValue(Set<E> solution) {\n        return solution.parallelStream()\n                .mapToDouble(edge -> edgeWeights.getOrDefault(edge, 1.0))\n                .sum();\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/arc/pageRank/DIAGRAM.md",
    "content": "# PageRank Feedback Arc Set (PageRankFAS) Algorithm\n\nBased on the paper *\"Computing a Feedback Arc Set Using PageRank\"* by Geladaris, Lionakis, and Tollis ([arXiv:2208.09234](https://arxiv.org/abs/2208.09234)).\n\n## High-Level Algorithm Flow\n\n```mermaid\nflowchart TD\n    A[\"**Input:** Directed Graph G(V, E)\"] --> B[\"Copy graph into<br/>working graph G'\"]\n    B --> C{\"Does G'<br/>have cycles?\"}\n    C -- No --> D[\"**Output:** Feedback Arc Set<br/>(set of removed edges)\"]\n    C -- Yes --> E[\"Find Strongly Connected<br/>Components (SCCs)<br/>using Kosaraju's algorithm\"]\n    E --> F[\"Filter to non-trivial SCCs<br/>(size > 1)\"]\n    F --> G[\"Process each SCC\"]\n    G --> H[\"Extract subgraph<br/>for this SCC\"]\n    H --> I[\"Build Line Digraph L(G)<br/>from SCC subgraph\"]\n    I --> J[\"Run PageRank<br/>on Line Digraph\"]\n    J --> K[\"Select edge with<br/>highest PageRank score\"]\n    K --> L[\"Remove edge from G'<br/>and add to FAS\"]\n    L --> C\n```\n\n## Line Digraph Construction\n\nEach edge in the original graph becomes a **vertex** in the line digraph. Edges in the line digraph represent adjacency (consecutive traversal) in the original graph.\n\n```mermaid\nflowchart TD\n    subgraph Original[\"**Original SCC Subgraph**\"]\n        direction LR\n        oA((A)) -->|e1| oB((B))\n        oB -->|e2| oC((C))\n        oC -->|e3| oA\n        oA -->|e4| oC\n    end\n\n    Original --> Transform[\"Transform: each original edge<br/>becomes a line digraph vertex\"]\n\n    subgraph Line[\"**Line Digraph L(G)**\"]\n        direction LR\n        le1[\"e1 (A→B)\"] -->|\"B is source of e2\"| le2[\"e2 (B→C)\"]\n        le2 -->|\"C is source of e3\"| le3[\"e3 (C→A)\"]\n        le2 -->|\"C is source of... none extra\"| le2x[ ]\n        le3 -->|\"A is source of e1\"| le1\n        le3 -->|\"A is source of e4\"| le4[\"e4 (A→C)\"]\n        le4 -->|\"C is source of e3\"| le3\n        le1 -.-> le4x[ ]\n    end\n\n    style le2x display:none\n    style le4x display:none\n```\n\n## Line Digraph Edge Creation (DFS-Based — Algorithm 3)\n\n```mermaid\nflowchart TD\n    S[\"Start DFS from<br/>arbitrary vertex v₀\"] --> V[\"Visit vertex v,<br/>mark as visited\"]\n    V --> OE[\"For each outgoing<br/>edge e of v\"]\n    OE --> LV[\"Get LineVertex<br/>for edge e\"]\n    LV --> PREV{\"Previous<br/>LineVertex<br/>exists?\"}\n    PREV -- Yes --> ADD_EDGE[\"Add edge:<br/>prevLineVertex → currentLineVertex<br/>in Line Digraph\"]\n    PREV -- No --> CHECK\n    ADD_EDGE --> CHECK{\"Target of e<br/>already visited?\"}\n    CHECK -- No --> REC[\"Recurse DFS on target<br/>with currentLineVertex as prev\"]\n    CHECK -- Yes --> BACK[\"Add edges from currentLineVertex<br/>to all LineVertices of<br/>target's outgoing edges<br/>(back-edge handling)\"]\n    REC --> OE\n    BACK --> OE\n```\n\n## PageRank Computation (Algorithm 4)\n\n```mermaid\nflowchart TD\n    INIT[\"Initialize all LineVertex scores<br/>score(v) = 1 / N\"] --> ITER{\"Iteration<br/>i < maxIterations?\"}\n    ITER -- No --> RESULT[\"Return PageRank scores<br/>for all LineVertices\"]\n    ITER -- Yes --> NEWMAP[\"Create new score map<br/>(all zeros)\"]\n    NEWMAP --> EACH[\"For each LineVertex v<br/>(in parallel)\"]\n    EACH --> SINK{\"v has outgoing<br/>neighbors?\"}\n    SINK -- \"No (sink)\" --> SELF[\"newScore(v) += score(v)<br/>(keep score on itself)\"]\n    SINK -- Yes --> DIST[\"Distribute score(v) equally<br/>among outgoing neighbors:<br/>each gets score(v) / outDegree(v)\"]\n    DIST --> MERGE[\"newScore(target) += share<br/>using atomic merge\"]\n    SELF --> SWAP\n    MERGE --> SWAP[\"Swap: currentScores = newScores\"]\n    SWAP --> ITER\n```\n\n## Selecting the Feedback Edge\n\n```mermaid\nflowchart LR\n    PR[\"PageRank scores<br/>on Line Digraph\"] --> MAX[\"Find LineVertex with<br/>**maximum** PageRank score\"]\n    MAX --> ORIG[\"Map back to<br/>original edge via<br/>LineVertex.getOriginalEdge()\"]\n    ORIG --> REMOVE[\"Remove edge from<br/>working graph &<br/>add to FAS\"]\n```\n\n## Class Relationships\n\n```mermaid\nclassDiagram\n    class PageRankFAS~V, E~ {\n        -Graph originalGraph\n        -int pageRankIterations\n        -Class edgeClass\n        +computeFeedbackArcSet() Set~E~\n        -processStronglyConnectedComponent(graph, scc) E\n        -createLineDigraph(graph) LineDigraph\n        -createLineDigraphEdges(graph, lineDigraph, map)\n        -createLineDigraphEdgesDFS(graph, lineDigraph, map, vertex, prev, visited)\n        -computePageRank(lineDigraph) Map\n        -applyOneIteration(vertices, lineDigraph, current, new)\n        -findStronglyConnectedComponents(graph) List\n        -hasCycles(graph) boolean\n        -createGraphCopy(original) Graph\n        -createSubgraph(graph, vertices) Graph\n    }\n\n    class LineDigraph~V, E~ {\n        -Set vertices\n        -Map adjacencyMap\n        -Map incomingMap\n        +addVertex(vertex) boolean\n        +addEdge(source, target) boolean\n        +vertexSet() Set\n        +getOutgoingNeighbors(vertex) Set\n        +getIncomingNeighbors(vertex) Set\n    }\n\n    class LineVertex~V, E~ {\n        -V source\n        -V target\n        -E originalEdge\n        +getSource() V\n        +getTarget() V\n        +getOriginalEdge() E\n    }\n\n    PageRankFAS --> LineDigraph : creates\n    PageRankFAS --> LineVertex : creates\n    LineDigraph o-- LineVertex : contains\n```\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/arc/pageRank/LineDigraph.java",
    "content": "package org.hjug.feedback.arc.pageRank;\n\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\n\n/**\n * Custom LineDigraph implementation that doesn't extend DefaultDirectedGraph.\n * Represents a directed graph where vertices are LineVertex objects representing\n * edges from the original graph, and edges represent adjacency relationships.\n */\nclass LineDigraph<V, E> {\n\n    // Internal storage for vertices and adjacency relationships\n    private final Set<LineVertex<V, E>> vertices;\n    private final Map<LineVertex<V, E>, Set<LineVertex<V, E>>> adjacencyMap;\n    private final Map<LineVertex<V, E>, Set<LineVertex<V, E>>> incomingMap;\n\n    /**\n     * Constructor for LineDigraph\n     */\n    public LineDigraph() {\n        this.vertices = ConcurrentHashMap.newKeySet();\n        this.adjacencyMap = new ConcurrentHashMap<>();\n        this.incomingMap = new ConcurrentHashMap<>();\n    }\n\n    /**\n     * Add a vertex to the line digraph\n     * @param vertex The LineVertex to add\n     * @return true if the vertex was added, false if it already existed\n     */\n    public boolean addVertex(LineVertex<V, E> vertex) {\n        if (vertices.add(vertex)) {\n            adjacencyMap.putIfAbsent(vertex, ConcurrentHashMap.newKeySet());\n            incomingMap.putIfAbsent(vertex, ConcurrentHashMap.newKeySet());\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Remove a vertex from the line digraph\n     * @param vertex The LineVertex to remove\n     * @return true if the vertex was removed, false if it didn't exist\n     */\n    public boolean removeVertex(LineVertex<V, E> vertex) {\n        if (vertices.remove(vertex)) {\n            // Remove all outgoing edges\n            Set<LineVertex<V, E>> outgoing = adjacencyMap.remove(vertex);\n            if (outgoing != null) {\n                outgoing.forEach(target -> incomingMap.get(target).remove(vertex));\n            }\n\n            // Remove all incoming edges\n            Set<LineVertex<V, E>> incoming = incomingMap.remove(vertex);\n            if (incoming != null) {\n                incoming.forEach(source -> adjacencyMap.get(source).remove(vertex));\n            }\n\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Add an edge between two vertices in the line digraph\n     * @param source The source LineVertex\n     * @param target The target LineVertex\n     * @return true if the edge was added, false if it already existed\n     */\n    public boolean addEdge(LineVertex<V, E> source, LineVertex<V, E> target) {\n        // Ensure both vertices exist\n        addVertex(source);\n        addVertex(target);\n\n        // Add edge if it doesn't exist\n        if (adjacencyMap.get(source).add(target)) {\n            incomingMap.get(target).add(source);\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Remove an edge between two vertices\n     * @param source The source LineVertex\n     * @param target The target LineVertex\n     * @return true if the edge was removed, false if it didn't exist\n     */\n    public boolean removeEdge(LineVertex<V, E> source, LineVertex<V, E> target) {\n        if (containsVertex(source) && containsVertex(target)) {\n            if (adjacencyMap.get(source).remove(target)) {\n                incomingMap.get(target).remove(source);\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Check if the digraph contains a specific vertex\n     * @param vertex The LineVertex to check\n     * @return true if the vertex exists, false otherwise\n     */\n    public boolean containsVertex(LineVertex<V, E> vertex) {\n        return vertices.contains(vertex);\n    }\n\n    /**\n     * Check if there's an edge between two vertices\n     * @param source The source LineVertex\n     * @param target The target LineVertex\n     * @return true if the edge exists, false otherwise\n     */\n    public boolean containsEdge(LineVertex<V, E> source, LineVertex<V, E> target) {\n        return containsVertex(source) && adjacencyMap.get(source).contains(target);\n    }\n\n    /**\n     * Get all vertices in the line digraph\n     * @return Set of all LineVertex objects\n     */\n    public Set<LineVertex<V, E>> vertexSet() {\n        return new HashSet<>(vertices);\n    }\n\n    /**\n     * Get the number of vertices\n     * @return Number of vertices in the digraph\n     */\n    public int vertexCount() {\n        return vertices.size();\n    }\n\n    /**\n     * Get the number of edges\n     * @return Total number of edges in the digraph\n     */\n    public int edgeCount() {\n        return adjacencyMap.values().stream().mapToInt(Set::size).sum();\n    }\n\n    /**\n     * Get all outgoing neighbors of a vertex\n     * @param vertex The source LineVertex\n     * @return Set of target LineVertex objects\n     */\n    public Set<LineVertex<V, E>> getOutgoingNeighbors(LineVertex<V, E> vertex) {\n        return adjacencyMap.getOrDefault(vertex, Collections.emptySet()).stream()\n                .collect(Collectors.toSet());\n    }\n\n    /**\n     * Get all incoming neighbors of a vertex\n     * @param vertex The target LineVertex\n     * @return Set of source LineVertex objects\n     */\n    public Set<LineVertex<V, E>> getIncomingNeighbors(LineVertex<V, E> vertex) {\n        return incomingMap.getOrDefault(vertex, Collections.emptySet()).stream().collect(Collectors.toSet());\n    }\n\n    /**\n     * Get all neighbors (both incoming and outgoing) of a vertex\n     * @param vertex The LineVertex\n     * @return Set of all neighboring LineVertex objects\n     */\n    public Set<LineVertex<V, E>> getAllNeighbors(LineVertex<V, E> vertex) {\n        Set<LineVertex<V, E>> neighbors = new HashSet<>();\n        neighbors.addAll(getOutgoingNeighbors(vertex));\n        neighbors.addAll(getIncomingNeighbors(vertex));\n        return neighbors;\n    }\n\n    /**\n     * Get the out-degree of a vertex\n     * @param vertex The LineVertex\n     * @return Number of outgoing edges\n     */\n    public int getOutDegree(LineVertex<V, E> vertex) {\n        return adjacencyMap.getOrDefault(vertex, Collections.emptySet()).size();\n    }\n\n    /**\n     * Get the in-degree of a vertex\n     * @param vertex The LineVertex\n     * @return Number of incoming edges\n     */\n    public int getInDegree(LineVertex<V, E> vertex) {\n        return incomingMap.getOrDefault(vertex, Collections.emptySet()).size();\n    }\n\n    /**\n     * Get the total degree (in + out) of a vertex\n     * @param vertex The LineVertex\n     * @return Total degree of the vertex\n     */\n    public int getTotalDegree(LineVertex<V, E> vertex) {\n        return getInDegree(vertex) + getOutDegree(vertex);\n    }\n\n    /**\n     * Check if the digraph is empty\n     * @return true if no vertices exist, false otherwise\n     */\n    public boolean isEmpty() {\n        return vertices.isEmpty();\n    }\n\n    /**\n     * Clear all vertices and edges from the digraph\n     */\n    public void clear() {\n        vertices.clear();\n        adjacencyMap.clear();\n        incomingMap.clear();\n    }\n\n    /**\n     * Get all vertices with no incoming edges (sources)\n     * @return Set of source LineVertex objects\n     */\n    public Set<LineVertex<V, E>> getSources() {\n        return vertices.stream().filter(vertex -> getInDegree(vertex) == 0).collect(Collectors.toSet());\n    }\n\n    /**\n     * Get all vertices with no outgoing edges (sinks)\n     * @return Set of sink LineVertex objects\n     */\n    public Set<LineVertex<V, E>> getSinks() {\n        return vertices.stream().filter(vertex -> getOutDegree(vertex) == 0).collect(Collectors.toSet());\n    }\n\n    /**\n     * Get vertices reachable from a given vertex (BFS traversal)\n     * @param startVertex The starting LineVertex\n     * @return Set of reachable LineVertex objects\n     */\n    public Set<LineVertex<V, E>> getReachableVertices(LineVertex<V, E> startVertex) {\n        Set<LineVertex<V, E>> reachable = new HashSet<>();\n        Queue<LineVertex<V, E>> queue = new LinkedList<>();\n\n        if (containsVertex(startVertex)) {\n            queue.offer(startVertex);\n            reachable.add(startVertex);\n\n            while (!queue.isEmpty()) {\n                LineVertex<V, E> current = queue.poll();\n                for (LineVertex<V, E> neighbor : getOutgoingNeighbors(current)) {\n                    if (reachable.add(neighbor)) {\n                        queue.offer(neighbor);\n                    }\n                }\n            }\n        }\n\n        return reachable;\n    }\n\n    /**\n     * Check if there's a path from source to target\n     * @param source The source LineVertex\n     * @param target The target LineVertex\n     * @return true if a path exists, false otherwise\n     */\n    public boolean hasPath(LineVertex<V, E> source, LineVertex<V, E> target) {\n        if (!containsVertex(source) || !containsVertex(target)) {\n            return false;\n        }\n\n        if (source.equals(target)) {\n            return true;\n        }\n\n        return getReachableVertices(source).contains(target);\n    }\n\n    /**\n     * Perform a topological sort of the digraph (if acyclic)\n     * @return List of vertices in topological order, or empty list if cyclic\n     */\n    public List<LineVertex<V, E>> topologicalSort() {\n        List<LineVertex<V, E>> result = new ArrayList<>();\n        Map<LineVertex<V, E>, Integer> inDegreeMap = new HashMap<>();\n        Queue<LineVertex<V, E>> queue = new LinkedList<>();\n\n        // Initialize in-degree map\n        for (LineVertex<V, E> vertex : vertices) {\n            inDegreeMap.put(vertex, getInDegree(vertex));\n            if (getInDegree(vertex) == 0) {\n                queue.offer(vertex);\n            }\n        }\n\n        // Process vertices with zero in-degree\n        while (!queue.isEmpty()) {\n            LineVertex<V, E> current = queue.poll();\n            result.add(current);\n\n            for (LineVertex<V, E> neighbor : getOutgoingNeighbors(current)) {\n                int newInDegree = inDegreeMap.get(neighbor) - 1;\n                inDegreeMap.put(neighbor, newInDegree);\n\n                if (newInDegree == 0) {\n                    queue.offer(neighbor);\n                }\n            }\n        }\n\n        // Return empty list if graph has cycles\n        return result.size() == vertices.size() ? result : Collections.emptyList();\n    }\n\n    /**\n     * Create a copy of this line digraph\n     * @return A new LineDigraph with the same structure\n     */\n    public LineDigraph<V, E> copy() {\n        LineDigraph<V, E> copy = new LineDigraph<>();\n\n        // Add all vertices\n        vertices.forEach(copy::addVertex);\n\n        // Add all edges\n        for (LineVertex<V, E> source : vertices) {\n            for (LineVertex<V, E> target : getOutgoingNeighbors(source)) {\n                copy.addEdge(source, target);\n            }\n        }\n\n        return copy;\n    }\n\n    /**\n     * Get statistics about the line digraph\n     * @return Map containing various statistics\n     */\n    public Map<String, Object> getStatistics() {\n        Map<String, Object> stats = new HashMap<>();\n\n        stats.put(\"vertexCount\", vertexCount());\n        stats.put(\"edgeCount\", edgeCount());\n        stats.put(\"sourceCount\", getSources().size());\n        stats.put(\"sinkCount\", getSinks().size());\n        stats.put(\"isEmpty\", isEmpty());\n\n        if (!isEmpty()) {\n            double avgOutDegree =\n                    vertices.stream().mapToInt(this::getOutDegree).average().orElse(0.0);\n\n            double avgInDegree =\n                    vertices.stream().mapToInt(this::getInDegree).average().orElse(0.0);\n\n            stats.put(\"avgOutDegree\", avgOutDegree);\n            stats.put(\"avgInDegree\", avgInDegree);\n            stats.put(\"density\", (double) edgeCount() / (vertexCount() * (vertexCount() - 1)));\n        }\n\n        return stats;\n    }\n\n    /**\n     * Convert to string representation for debugging\n     */\n    @Override\n    public String toString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"LineDigraph{\");\n        sb.append(\"vertices=\").append(vertices.size());\n        sb.append(\", edges=\").append(edgeCount());\n        sb.append(\"}\");\n        return sb.toString();\n    }\n\n    /**\n     * Get detailed string representation with all edges\n     * @return Detailed string representation\n     */\n    public String toDetailedString() {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"LineDigraph Details:\\n\");\n        sb.append(\"Vertices: \").append(vertices.size()).append(\"\\n\");\n        sb.append(\"Edges: \").append(edgeCount()).append(\"\\n\\n\");\n\n        for (LineVertex<V, E> vertex : vertices) {\n            sb.append(vertex).append(\" -> \");\n            Set<LineVertex<V, E>> outgoing = getOutgoingNeighbors(vertex);\n            if (outgoing.isEmpty()) {\n                sb.append(\"[]\");\n            } else {\n                sb.append(outgoing);\n            }\n            sb.append(\"\\n\");\n        }\n\n        return sb.toString();\n    }\n\n    /**\n     * Validate the internal consistency of the digraph\n     * @return true if consistent, false otherwise\n     */\n    public boolean validateConsistency() {\n        // Check that every outgoing edge has a corresponding incoming edge\n        for (LineVertex<V, E> source : vertices) {\n            for (LineVertex<V, E> target : getOutgoingNeighbors(source)) {\n                if (!getIncomingNeighbors(target).contains(source)) {\n                    return false;\n                }\n            }\n        }\n\n        // Check that every incoming edge has a corresponding outgoing edge\n        for (LineVertex<V, E> target : vertices) {\n            for (LineVertex<V, E> source : getIncomingNeighbors(target)) {\n                if (!getOutgoingNeighbors(source).contains(target)) {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/arc/pageRank/PageRankFAS.java",
    "content": "package org.hjug.feedback.arc.pageRank;\n\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.connectivity.KosarajuStrongConnectivityInspector;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.DefaultDirectedGraph;\n\n/**\n * PageRankFAS - A PageRank-based algorithm for computing Feedback Arc Set\n * Based on the paper \"Computing a Feedback Arc Set Using PageRank\" by\n * Geladaris, Lionakis, and Tollis\n * Generated by Perplexity AI and modified.\n * Based on https://arxiv.org/abs/2208.09234\n * https://doi.org/10.48550/arXiv.2208.09234\n */\n@Slf4j\npublic class PageRankFAS<V, E> {\n\n    private static final int DEFAULT_PAGERANK_ITERATIONS = 5;\n    private static final double CONVERGENCE_THRESHOLD = 1e-6;\n\n    private final Graph<V, E> originalGraph;\n    private final int pageRankIterations;\n    private final Class<E> edgeClass;\n\n    /**\n     * Constructor for PageRankFAS algorithm\n     *\n     * @param graph         The input directed graph\n     * @param edgeTypeToken\n     */\n    public PageRankFAS(Graph<V, E> graph, SuperTypeToken<E> edgeTypeToken) {\n        this(graph, DEFAULT_PAGERANK_ITERATIONS, edgeTypeToken);\n    }\n\n    /**\n     * Constructor with custom PageRank iterations\n     *\n     * @param graph              The input directed graph\n     * @param pageRankIterations Number of PageRank iterations\n     * @param edgeTypeToken\n     */\n    public PageRankFAS(Graph<V, E> graph, int pageRankIterations, SuperTypeToken<E> edgeTypeToken) {\n        this.originalGraph = graph;\n        this.pageRankIterations = pageRankIterations;\n        this.edgeClass = edgeTypeToken.getClassFromTypeToken();\n    }\n\n    /**\n     * Main method to compute the Feedback Arc Set\n     * @return Set of edges that form the feedback arc set\n     */\n    public Set<E> computeFeedbackArcSet() {\n        Set<E> feedbackArcSet = new HashSet<>();\n\n        // Create a working copy of the graph\n        Graph<V, E> workingGraph = createGraphCopy(originalGraph);\n\n        // Continue until the graph becomes acyclic\n        while (hasCycles(workingGraph)) {\n            // Find strongly connected components\n            List<Set<V>> sccs = findStronglyConnectedComponents(workingGraph);\n\n            // Process each SCC\n            sccs.stream()\n                    .filter(scc -> scc.size() > 1) // Only non-trivial SCCs can have cycles\n                    .forEach(scc -> {\n                        E edgeToRemove = processStronglyConnectedComponent(workingGraph, scc);\n                        if (edgeToRemove != null) {\n                            synchronized (feedbackArcSet) {\n                                feedbackArcSet.add(edgeToRemove);\n                                workingGraph.removeEdge(edgeToRemove);\n                            }\n                        }\n                    });\n        }\n        return feedbackArcSet;\n    }\n\n    /**\n     * Process a single strongly connected component\n     * @param graph The working graph\n     * @param scc The strongly connected component vertices\n     * @return The edge with the highest PageRank score to remove\n     */\n    private E processStronglyConnectedComponent(Graph<V, E> graph, Set<V> scc) {\n        // Create subgraph for this SCC\n        Graph<V, E> sccGraph = createSubgraph(graph, scc);\n\n        // Create line digraph using the new custom implementation\n        LineDigraph<V, E> lineDigraph = createLineDigraph(sccGraph);\n\n        // Run PageRank on line digraph\n        Map<LineVertex<V, E>, Double> pageRankScores = computePageRank(lineDigraph);\n\n        // Find the edge (line vertex) with highest PageRank score\n        return pageRankScores.entrySet().parallelStream()\n                .max(Map.Entry.comparingByValue())\n                .map(entry -> entry.getKey().getOriginalEdge())\n                .orElse(null);\n    }\n\n    /**\n     * Create line digraph from the input graph using custom LineDigraph implementation\n     * @param graph Input graph\n     * @return LineDigraph representation\n     */\n    private LineDigraph<V, E> createLineDigraph(Graph<V, E> graph) {\n        LineDigraph<V, E> lineDigraph = new LineDigraph<>();\n\n        // Create nodes in line digraph (one for each edge in original graph)\n        Map<E, LineVertex<V, E>> edgeToLineVertex = new ConcurrentHashMap<>();\n\n        graph.edgeSet().parallelStream().forEach(edge -> {\n            V source = graph.getEdgeSource(edge);\n            V target = graph.getEdgeTarget(edge);\n            LineVertex<V, E> lineVertex = new LineVertex<>(source, target, edge);\n            edgeToLineVertex.put(edge, lineVertex);\n            lineDigraph.addVertex(lineVertex);\n        });\n\n        // Create edges in line digraph using DFS-based approach from the paper\n        createLineDigraphEdges(graph, lineDigraph, edgeToLineVertex);\n\n        return lineDigraph;\n    }\n\n    /**\n     * Create edges in line digraph based on Algorithm 3 from the paper\n     * Updated to use custom LineDigraph methods\n     */\n    private void createLineDigraphEdges(\n            Graph<V, E> graph, LineDigraph<V, E> lineDigraph, Map<E, LineVertex<V, E>> edgeToLineVertex) {\n        Set<V> visited = ConcurrentHashMap.newKeySet();\n\n        // Start DFS from a random vertex if graph is not empty\n        if (!graph.vertexSet().isEmpty()) {\n            V startVertex = graph.vertexSet().iterator().next();\n            createLineDigraphEdgesDFS(graph, lineDigraph, edgeToLineVertex, startVertex, null, visited);\n        }\n    }\n\n    /**\n     * DFS-based creation of line digraph edges (Algorithm 3 implementation)\n     * Updated to use custom LineDigraph.addEdge method\n     */\n    private void createLineDigraphEdgesDFS(\n            Graph<V, E> graph,\n            LineDigraph<V, E> lineDigraph,\n            Map<E, LineVertex<V, E>> edgeToLineVertex,\n            V vertex,\n            LineVertex<V, E> prevLineVertex,\n            Set<V> visited) {\n        visited.add(vertex);\n\n        // Get outgoing edges from current vertex\n        Set<E> outgoingEdges = graph.outgoingEdgesOf(vertex);\n\n        for (E edge : outgoingEdges) {\n            V target = graph.getEdgeTarget(edge);\n            LineVertex<V, E> currentLineVertex = edgeToLineVertex.get(edge);\n\n            // if currentLineVertex is null, skip processing\n            // for this edge since it will result in an NPE\n            if (currentLineVertex == null) {\n                continue;\n            }\n\n            // Add edge from previous line vertex to current (if prev exists)\n            if (prevLineVertex != null) {\n                lineDigraph.addEdge(prevLineVertex, currentLineVertex);\n            }\n\n            if (!visited.contains(target)) {\n                // Continue DFS\n                createLineDigraphEdgesDFS(graph, lineDigraph, edgeToLineVertex, target, currentLineVertex, visited);\n            } else {\n                // Target is already visited - add edges to all line vertices originating from target\n                graph.outgoingEdgesOf(target).stream()\n                        .map(edgeToLineVertex::get)\n                        .filter(Objects::nonNull)\n                        .forEach(targetLineVertex -> lineDigraph.addEdge(currentLineVertex, targetLineVertex));\n            }\n        }\n    }\n\n    /**\n     * Compute PageRank scores on the line digraph (Algorithm 4 implementation)\n     * @param lineDigraph The line digraph\n     * @return Map of line vertices to their PageRank scores\n     */\n    private Map<LineVertex<V, E>, Double> computePageRank(LineDigraph<V, E> lineDigraph) {\n        Set<LineVertex<V, E>> vertices = lineDigraph.vertexSet();\n        int numVertices = vertices.size();\n\n        if (numVertices == 0) return new HashMap<>();\n\n        // Initialize PageRank scores\n        Map<LineVertex<V, E>, Double> currentScores =\n                new ConcurrentHashMap<>(Math.max(16, (int) (numVertices / 0.75f) + 1));\n\n        final double initialScore = 1.0 / numVertices;\n        // No lambdas here, so nothing captures a non-final variable\n        for (LineVertex<V, E> v : vertices) {\n            currentScores.put(v, initialScore);\n        }\n\n        // Run PageRank iterations\n        for (int iteration = 0; iteration < pageRankIterations; iteration++) {\n            // Fresh map each iteration; pre-seed zeros so all vertices exist in the map\n            ConcurrentMap<LineVertex<V, E>, Double> newScores = new ConcurrentHashMap<>(currentScores.size());\n\n            for (LineVertex<V, E> v : vertices) {\n                newScores.put(v, 0.0);\n            }\n\n            // Do one iteration in parallel; lambdas only see method parameters (effectively final)\n            applyOneIteration(vertices, lineDigraph, currentScores, newScores);\n\n            // Swap for next iteration (this reassigns local variables, not captured by lambdas)\n            currentScores = newScores;\n        }\n\n        return currentScores;\n    }\n\n    private void applyOneIteration(\n            Set<LineVertex<V, E>> vertices,\n            LineDigraph<V, E> lineDigraph,\n            Map<LineVertex<V, E>, Double> currentScores,\n            ConcurrentMap<LineVertex<V, E>, Double> newScores) {\n\n        vertices.parallelStream().forEach(vertex -> {\n            double score = currentScores.get(vertex);\n            Set<LineVertex<V, E>> outgoing = lineDigraph.getOutgoingNeighbors(vertex);\n\n            if (outgoing.isEmpty()) {\n                // Sink: keep score on itself\n                newScores.merge(vertex, score, Double::sum);\n            } else {\n                double scorePerEdge = score / outgoing.size();\n                // Inner loop kept sequential: nested parallel often hurts more than it helps\n                for (LineVertex<V, E> target : outgoing) {\n                    newScores.merge(target, scorePerEdge, Double::sum);\n                }\n            }\n        });\n    }\n\n    /**\n     * Find strongly connected components using Kosaraju's algorithm\n     */\n    private List<Set<V>> findStronglyConnectedComponents(Graph<V, E> graph) {\n        KosarajuStrongConnectivityInspector<V, E> inspector = new KosarajuStrongConnectivityInspector<>(graph);\n        return inspector.stronglyConnectedSets();\n    }\n\n    /**\n     * Check if graph has cycles\n     */\n    private boolean hasCycles(Graph<V, E> graph) {\n        CycleDetector<V, E> detector = new CycleDetector<>(graph);\n        return detector.detectCycles();\n    }\n\n    /**\n     * Create a copy of the graph\n     */\n    private Graph<V, E> createGraphCopy(Graph<V, E> original) {\n        Graph<V, E> copy = new DefaultDirectedGraph<>(edgeClass);\n\n        // Add vertices\n        original.vertexSet().forEach(copy::addVertex);\n\n        // Add edges\n        original.edgeSet().forEach(edge -> {\n            V source = original.getEdgeSource(edge);\n            V target = original.getEdgeTarget(edge);\n            copy.addEdge(source, target, edge);\n        });\n\n        return copy;\n    }\n\n    /**\n     * Create subgraph containing only specified vertices and their edges\n     */\n    private Graph<V, E> createSubgraph(Graph<V, E> graph, Set<V> vertices) {\n        Graph<V, E> subgraph = new DefaultDirectedGraph<>(edgeClass);\n\n        // Add vertices\n        vertices.forEach(subgraph::addVertex);\n\n        // Add edges between vertices in the set\n        graph.edgeSet().stream()\n                .filter(edge ->\n                        vertices.contains(graph.getEdgeSource(edge)) && vertices.contains(graph.getEdgeTarget(edge)))\n                .forEach(edge -> {\n                    V source = graph.getEdgeSource(edge);\n                    V target = graph.getEdgeTarget(edge);\n                    subgraph.addEdge(source, target, edge);\n                });\n\n        // ConcurrenModificationException\n        // at org.hjug.feedback.arc.pageRank.PageRankFAS.createSubgraph (PageRankFAS.java:302)\n        //    at org.hjug.feedback.arc.pageRank.PageRankFAS.processStronglyConnectedComponent (PageRankFAS.java:92)\n        //    at org.hjug.feedback.arc.pageRank.PageRankFAS.lambda$computeFeedbackArcSet$1 (PageRankFAS.java:71)\n\n        return subgraph;\n    }\n\n    /**\n     * Get detailed statistics about the algorithm execution\n     * @return Map containing execution statistics\n     */\n    public Map<String, Object> getExecutionStatistics(Graph<V, E> graph) {\n        Map<String, Object> stats = new HashMap<>();\n\n        stats.put(\"originalVertices\", graph.vertexSet().size());\n        stats.put(\"originalEdges\", graph.edgeSet().size());\n        stats.put(\"pageRankIterations\", pageRankIterations);\n\n        // Analyze SCCs\n        List<Set<V>> sccs = findStronglyConnectedComponents(graph);\n        stats.put(\"sccCount\", sccs.size());\n        stats.put(\n                \"trivialSCCs\",\n                sccs.stream().mapToInt(scc -> scc.size() == 1 ? 1 : 0).sum());\n        stats.put(\n                \"nonTrivialSCCs\",\n                sccs.stream().mapToInt(scc -> scc.size() > 1 ? 1 : 0).sum());\n\n        // Find largest SCC\n        int maxSCCSize = sccs.stream().mapToInt(Set::size).max().orElse(0);\n        stats.put(\"largestSCCSize\", maxSCCSize);\n\n        return stats;\n    }\n}\n\n/**\n * Represents a vertex in the line digraph (corresponds to an edge in original graph)\n */\nclass LineVertex<V, E> {\n    private final V source;\n    private final V target;\n    private final E originalEdge;\n\n    public LineVertex(V source, V target, E originalEdge) {\n        this.source = source;\n        this.target = target;\n        this.originalEdge = originalEdge;\n    }\n\n    public V getSource() {\n        return source;\n    }\n\n    public V getTarget() {\n        return target;\n    }\n\n    public E getOriginalEdge() {\n        return originalEdge;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (this == obj) return true;\n        if (!(obj instanceof LineVertex)) return false;\n        LineVertex<?, ?> other = (LineVertex<?, ?>) obj;\n        return Objects.equals(originalEdge, other.originalEdge);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(originalEdge);\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"LineVertex(%s->%s)\", source, target);\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/approximate/FeedbackVertexSetResult.java",
    "content": "package org.hjug.feedback.vertex.approximate;\n\nimport java.util.Set;\n\n/**\n * Result container for the Feedback Vertex Set algorithm\n */\npublic class FeedbackVertexSetResult<V> {\n    private final Set<V> feedbackVertices;\n\n    public FeedbackVertexSetResult(Set<V> feedbackVertices) {\n        this.feedbackVertices = feedbackVertices;\n    }\n\n    public Set<V> getFeedbackVertices() {\n        return feedbackVertices;\n    }\n\n    public int size() {\n        return feedbackVertices.size();\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\n                \"FeedbackVertexSetResult{vertices=%s, size=%d}\", feedbackVertices, feedbackVertices.size());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/approximate/FeedbackVertexSetSolver.java",
    "content": "package org.hjug.feedback.vertex.approximate;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.connectivity.KosarajuStrongConnectivityInspector;\nimport org.jgrapht.alg.interfaces.ShortestPathAlgorithm;\nimport org.jgrapht.alg.interfaces.StrongConnectivityAlgorithm;\nimport org.jgrapht.alg.shortestpath.DijkstraShortestPath;\nimport org.jgrapht.graph.AsSubgraph;\nimport org.jgrapht.graph.AsWeightedGraph;\n\n/**\n * Parallel implementation of the Feedback Vertex Set algorithm\n * Based on \"Approximating Minimum Feedback Sets and Multicuts in Directed Graphs\"\n * DOI:10.1007/PL00009191\n * https://www.researchgate.net/publication/227278349_Approximating_Minimum_Feedback_Sets_and_Multicuts_in_Directed_Graphs\n * Generated by Perplexity.ai's Research model\n */\npublic class FeedbackVertexSetSolver<V, E> {\n\n    private final Graph<V, E> graph;\n    private final Set<V> specialVertices;\n    private final Map<V, Double> vertexWeights;\n    private final Map<V, Double> fractionalSolution;\n    private final double epsilon;\n    private final ForkJoinPool forkJoinPool;\n\n    public FeedbackVertexSetSolver(\n            Graph<V, E> graph, Set<V> specialVertices, Map<V, Double> vertexWeights, double epsilon) {\n        this.graph = graph;\n        this.specialVertices = specialVertices != null ? specialVertices : new HashSet<>(graph.vertexSet());\n        this.vertexWeights = vertexWeights != null ? vertexWeights : createUniformWeights();\n        this.epsilon = epsilon;\n        this.forkJoinPool = ForkJoinPool.commonPool();\n        this.fractionalSolution = computeFractionalSolution();\n    }\n\n    /**\n     * Creates uniform weights for all vertices when no weights are provided[3]\n     */\n    private Map<V, Double> createUniformWeights() {\n        Map<V, Double> weights = new ConcurrentHashMap<>();\n        graph.vertexSet().parallelStream().forEach(v -> weights.put(v, 1.0));\n        return weights;\n    }\n\n    /**\n     * Computes the fractional solution using the combinatorial algorithm from the paper[1]\n     */\n    private Map<V, Double> computeFractionalSolution() {\n        Map<V, Double> y = new ConcurrentHashMap<>();\n        graph.vertexSet().parallelStream().forEach(v -> y.put(v, 0.0));\n\n        AtomicInteger iteration = new AtomicInteger(0);\n\n        while (hasInterestingCycle()) {\n            // Compute cycle counts for each vertex in parallel[9]\n            Map<V, Long> cycleCounts = computeCycleCounts();\n\n            // Find vertex minimizing w(v)/f(v) using parallel streams[10]\n            Optional<V> minVertex = graph.vertexSet().parallelStream()\n                    .filter(v -> cycleCounts.getOrDefault(v, 0L) > 0)\n                    .min(Comparator.comparingDouble(v -> vertexWeights.get(v) / cycleCounts.get(v)));\n\n            if (!minVertex.isPresent()) break;\n\n            V vertex = minVertex.get();\n            double increment = vertexWeights.get(vertex) / cycleCounts.get(vertex);\n\n            // Update fractional solution atomically\n            y.compute(vertex, (k, val) -> Math.min(1.0, val + increment * (1 + epsilon)));\n\n            iteration.incrementAndGet();\n            if (iteration.get() > graph.vertexSet().size() * 10) break; // Safety check\n        }\n\n        return y;\n    }\n\n    /**\n     * Computes cycle counts for vertices using strongly connected components[9][12]\n     */\n    private Map<V, Long> computeCycleCounts() {\n        Map<V, Long> counts = new ConcurrentHashMap<>();\n\n        StrongConnectivityAlgorithm<V, E> scAlg = new KosarajuStrongConnectivityInspector<>(graph);\n\n        scAlg.stronglyConnectedSets().parallelStream()\n                .filter(this::isInterestingComponent)\n                .forEach(scc -> {\n                    scc.parallelStream().forEach(v -> counts.merge(v, 1L, Long::sum));\n                });\n\n        return counts;\n    }\n\n    /**\n     * Checks if a strongly connected component contains special vertices and forms cycles[1]\n     */\n    private boolean isInterestingComponent(Set<V> scc) {\n        boolean containsSpecial = scc.stream().anyMatch(specialVertices::contains);\n        boolean hasCycle = scc.size() > 1\n                || (scc.size() == 1\n                        && graph.containsEdge(\n                                scc.iterator().next(), scc.iterator().next()));\n        return containsSpecial && hasCycle;\n    }\n\n    /**\n     * Checks if the graph contains interesting cycles[1]\n     */\n    private boolean hasInterestingCycle() {\n        StrongConnectivityAlgorithm<V, E> scAlg = new KosarajuStrongConnectivityInspector<>(graph);\n\n        return scAlg.stronglyConnectedSets().parallelStream().anyMatch(this::isInterestingComponent);\n    }\n\n    /**\n     * Main solving method implementing the recursive decomposition algorithm[1]\n     */\n    public FeedbackVertexSetResult<V> solve() {\n        return solveRecursive(graph, specialVertices);\n    }\n\n    /**\n     * Recursive solver using graph decomposition and parallel processing[1][25]\n     */\n    private FeedbackVertexSetResult<V> solveRecursive(Graph<V, E> currentGraph, Set<V> currentSpecial) {\n        if (!hasInterestingCycleInSubgraph(currentGraph, currentSpecial)) {\n            return new FeedbackVertexSetResult<>(new HashSet<>());\n        }\n\n        // Select source vertex from special vertices\n        V source = currentSpecial.iterator().next();\n\n        // Compute distances using transformed edge weights[20][21]\n        Map<V, Double> distances = computeDistances(currentGraph, source);\n\n        // Find all distinct distance values\n        List<Double> distValues =\n                distances.values().parallelStream().distinct().sorted().collect(Collectors.toList());\n\n        // Evaluate cut candidates in parallel[10]\n        List<CutCandidate<V>> candidates = distValues.parallelStream()\n                .map(dist -> evaluateCut(currentGraph, distances, dist))\n                .filter(Objects::nonNull)\n                .collect(Collectors.toList());\n\n        if (candidates.isEmpty()) {\n            // Fallback: select vertex with maximum degree\n            Optional<V> maxDegreeVertex = currentGraph.vertexSet().parallelStream()\n                    .max(Comparator.comparingInt(v -> currentGraph.inDegreeOf(v) + currentGraph.outDegreeOf(v)));\n\n            if (maxDegreeVertex.isPresent()) {\n                Set<V> solution = new HashSet<>();\n                solution.add(maxDegreeVertex.get());\n                return new FeedbackVertexSetResult<>(solution);\n            }\n            return new FeedbackVertexSetResult<>(new HashSet<>());\n        }\n\n        // Select best cut candidate\n        CutCandidate<V> bestCandidate = candidates.parallelStream()\n                .min(Comparator.comparingDouble(c -> c.ratio))\n                .orElseThrow();\n\n        // Create subgraphs using AsSubgraph[24]\n        Set<V> leftVertices = createLeftPartition(currentGraph, distances, bestCandidate.distance);\n        Set<V> rightVertices = createRightPartition(currentGraph, distances, bestCandidate.distance);\n\n        // Recursive solve using ForkJoinPool[25]\n        CompletableFuture<FeedbackVertexSetResult<V>> leftFuture = CompletableFuture.supplyAsync(\n                () -> {\n                    if (!leftVertices.isEmpty()) {\n                        Graph<V, E> leftGraph = new AsSubgraph<>(currentGraph, leftVertices);\n                        Set<V> leftSpecial = intersection(currentSpecial, leftVertices);\n                        return solveRecursive(leftGraph, leftSpecial);\n                    }\n                    return new FeedbackVertexSetResult<>(new HashSet<>());\n                },\n                forkJoinPool);\n\n        CompletableFuture<FeedbackVertexSetResult<V>> rightFuture = CompletableFuture.supplyAsync(\n                () -> {\n                    if (!rightVertices.isEmpty()) {\n                        Graph<V, E> rightGraph = new AsSubgraph<>(currentGraph, rightVertices);\n                        Set<V> rightSpecial = intersection(currentSpecial, rightVertices);\n                        return solveRecursive(rightGraph, rightSpecial);\n                    }\n                    return new FeedbackVertexSetResult<>(new HashSet<>());\n                },\n                forkJoinPool);\n\n        // Combine results\n        try {\n            FeedbackVertexSetResult<V> leftResult = leftFuture.get();\n            FeedbackVertexSetResult<V> rightResult = rightFuture.get();\n\n            Set<V> solution = new HashSet<>(bestCandidate.cut);\n            solution.addAll(leftResult.getFeedbackVertices());\n            solution.addAll(rightResult.getFeedbackVertices());\n\n            return new FeedbackVertexSetResult<>(solution);\n        } catch (InterruptedException | ExecutionException e) {\n            Thread.currentThread().interrupt();\n            throw new RuntimeException(\"Parallel execution failed\", e);\n        }\n    }\n\n    /**\n     * Computes shortest path distances using Dijkstra algorithm with transformed weights[20][26]\n     */\n    private Map<V, Double> computeDistances(Graph<V, E> graph, V source) {\n        // Transform to weighted graph using fractional solution values[26]\n        Function<E, Double> weightFunction = edge -> {\n            V target = graph.getEdgeTarget(edge);\n            return fractionalSolution.getOrDefault(target, 0.0);\n        };\n\n        AsWeightedGraph<V, E> weightedGraph = new AsWeightedGraph<>(graph, weightFunction, false, false);\n\n        // Compute shortest paths using Dijkstra[20]\n        DijkstraShortestPath<V, E> dijkstra = new DijkstraShortestPath<>(weightedGraph);\n        ShortestPathAlgorithm.SingleSourcePaths<V, E> paths = dijkstra.getPaths(source);\n\n        Map<V, Double> distances = new ConcurrentHashMap<>();\n        graph.vertexSet().parallelStream().forEach(v -> {\n            double distance = paths.getWeight(v);\n            if (Double.isInfinite(distance)) {\n                distance = Double.MAX_VALUE;\n            }\n            distances.put(v, distance + fractionalSolution.getOrDefault(source, 0.0));\n        });\n\n        return distances;\n    }\n\n    /**\n     * Evaluates a cut candidate based on the ratio of actual weight to fractional weight[1]\n     */\n    private CutCandidate<V> evaluateCut(Graph<V, E> graph, Map<V, Double> distances, double cutDistance) {\n        Set<V> cut = graph.vertexSet().parallelStream()\n                .filter(v -> Math.abs(distances.get(v) - cutDistance) < 1e-10)\n                .collect(Collectors.toSet());\n\n        if (cut.isEmpty()) return null;\n\n        double actualWeight = cut.parallelStream()\n                .mapToDouble(v -> vertexWeights.getOrDefault(v, 1.0))\n                .sum();\n\n        double fractionalWeight = cut.parallelStream()\n                .mapToDouble(v -> fractionalSolution.getOrDefault(v, 0.0))\n                .sum();\n\n        if (fractionalWeight <= 1e-10) return null;\n\n        return new CutCandidate<>(cut, actualWeight / fractionalWeight, cutDistance);\n    }\n\n    /**\n     * Creates left partition of vertices[1]\n     */\n    private Set<V> createLeftPartition(Graph<V, E> graph, Map<V, Double> distances, double cutDistance) {\n        return graph.vertexSet().parallelStream()\n                .filter(v -> distances.get(v) < cutDistance - 1e-10)\n                .collect(Collectors.toSet());\n    }\n\n    /**\n     * Creates right partition of vertices[1]\n     */\n    private Set<V> createRightPartition(Graph<V, E> graph, Map<V, Double> distances, double cutDistance) {\n        return graph.vertexSet().parallelStream()\n                .filter(v -> distances.get(v) > cutDistance + 1e-10)\n                .collect(Collectors.toSet());\n    }\n\n    /**\n     * Checks for interesting cycles in a subgraph[9]\n     */\n    private boolean hasInterestingCycleInSubgraph(Graph<V, E> subgraph, Set<V> special) {\n        if (subgraph.vertexSet().isEmpty()) return false;\n\n        StrongConnectivityAlgorithm<V, E> scAlg = new KosarajuStrongConnectivityInspector<>(subgraph);\n\n        return scAlg.stronglyConnectedSets().parallelStream().anyMatch(scc -> {\n            boolean containsSpecial = scc.stream().anyMatch(special::contains);\n            boolean hasCycle = scc.size() > 1\n                    || (scc.size() == 1\n                            && subgraph.containsEdge(\n                                    scc.iterator().next(), scc.iterator().next()));\n            return containsSpecial && hasCycle;\n        });\n    }\n\n    /**\n     * Computes intersection of two sets using parallel streams[10]\n     */\n    private Set<V> intersection(Set<V> set1, Set<V> set2) {\n        return set1.parallelStream().filter(set2::contains).collect(Collectors.toSet());\n    }\n\n    /**\n     * Cut candidate data structure[1]\n     */\n    private static class CutCandidate<V> {\n        final Set<V> cut;\n        final double ratio;\n        final double distance;\n\n        CutCandidate(Set<V> cut, double ratio, double distance) {\n            this.cut = cut;\n            this.ratio = ratio;\n            this.distance = distance;\n        }\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/kernelized/DIAGRAM.md",
    "content": "# Kernelized Directed Feedback Vertex Set (DFVS) Algorithm\n\nBased on: *\"Wannabe Bounded Treewidth Graphs Admit a Polynomial Kernel for DFVS\"* (Lokshtanov et al.)  \nhttps://doi.org/10.1145/3711669\n## Class Architecture\n\n```mermaid\nclassDiagram\n    direction TB\n\n    class DirectedFeedbackVertexSetSolver~V,E~ {\n        -Graph graph\n        -Set~V~ modulator\n        -Map vertexWeights\n        -int eta\n        -Set~V~ remainder\n        -Map zones\n        -Map kDfvsRepresentatives\n        -int k\n        +solve() DirectedFeedbackVertexSetResult\n        +solve(int k) DirectedFeedbackVertexSetResult\n        -computeZoneDecomposition(k)\n        -computeKDfvsRepresentatives(k)\n        -solveWithReductionRules(k)\n    }\n\n    class EnhancedParameterComputer~V,E~ {\n        -TreewidthComputer treewidthComputer\n        -FeedbackVertexSetComputer fvsComputer\n        -ModulatorComputer modulatorComputer\n        +computeOptimalParameters(graph, maxSize) EnhancedParameters\n        +computeParameters(graph, modulator) EnhancedParameters\n    }\n\n    class ParameterComputer~V,E~ {\n        -TreewidthComputer treewidthComputer\n        -FeedbackVertexSetComputer fvsComputer\n        +computeParameters(graph) Parameters\n        +computeParametersWithOptimalModulator(graph, maxSize) Parameters\n    }\n\n    class FeedbackVertexSetComputer~V,E~ {\n        +computeK(graph) int\n        +greedyFeedbackVertexSet(graph) Set~V~\n        -stronglyConnectedComponentsBasedFVS(graph) Set~V~\n        -degreeBasedFeedbackVertexSet(graph) Set~V~\n        -localSearchFeedbackVertexSet(graph) Set~V~\n    }\n\n    class TreewidthComputer~V,E~ {\n        +computeEta(graph, modulator) int\n        -minDegreeEliminationTreewidth(graph) int\n        -fillInHeuristicTreewidth(graph) int\n        -maxCliqueTreewidth(graph) int\n        -greedyTriangulationTreewidth(graph) int\n    }\n\n    class ModulatorComputer~V,E~ {\n        +computeModulator(graph, targetTw, maxSize) ModulatorResult\n        -computeGreedyDegreeModulator() Set~V~\n        -computeFeedbackVertexSetModulator() Set~V~\n        -computeTreewidthDecompositionModulator() Set~V~\n        -computeHighDegreeVertexModulator() Set~V~\n        -computeBottleneckVertexModulator() Set~V~\n    }\n\n    class DirectedFeedbackVertexSetResult~V~ {\n        -Set~V~ feedbackVertices\n        +getFeedbackVertices() Set~V~\n        +size() int\n    }\n\n    EnhancedParameterComputer --> TreewidthComputer : uses\n    EnhancedParameterComputer --> FeedbackVertexSetComputer : uses\n    EnhancedParameterComputer --> ModulatorComputer : uses\n    ParameterComputer --> TreewidthComputer : uses\n    ParameterComputer --> FeedbackVertexSetComputer : uses\n    ModulatorComputer --> TreewidthComputer : uses\n    ModulatorComputer --> FeedbackVertexSetComputer : uses\n    DirectedFeedbackVertexSetSolver --> DirectedFeedbackVertexSetResult : produces\n```\n\n## Algorithm Overview — Three-Phase Kernelization\n\n```mermaid\nflowchart TD\n    Start([\"`**Input:** Directed graph G, modulator M, treewidth η, weights`\"]) --> SCC\n\n    SCC[\"`**Compute default k**\n    Kosaraju SCC count as lower bound`\"]\n    SCC --> P1\n\n    subgraph P1[\"Phase 1 — Zone Decomposition\"]\n        direction TB\n        P1A[\"`**Remove modulator** from graph\n        G' = G ∖ M`\"]\n        P1A --> P1B[\"`**Compute minimal FVS** S\n        in G' (greedy, up to k vertices)`\"]\n        P1B --> P1C{\"|S| > k?\"}\n        P1C -- Yes --> NO_INST([\"`**NO-instance**\n        return empty`\"])\n        P1C -- No --> P1D[\"`**Compute flow-blocker F**\n        For each modulator pair (u,v):\n        find min vertex cut ≤ k in G'`\"]\n        P1D --> P1E[\"`**Compute remainder R**\n        R = S ∪ F\n        Bound: |R| ≤ 2k(η+1)(|M|²+1)`\"]\n        P1E --> P1F[\"`**Partition into zones**\n        Vertices not in M or R →\n        connected components = zones`\"]\n    end\n\n    P1 --> P2\n\n    subgraph P2[\"Phase 2 — k-DFVS Representative Marking\"]\n        direction TB\n        P2A[\"`**For each zone Z** (in parallel):`\"]\n        P2A --> P2B[\"`Compute **SCCs** within zone subgraph`\"]\n        P2B --> P2C[\"`From each non-trivial SCC,\n        select **highest-degree vertex**\n        as representative`\"]\n        P2C --> P2D[\"`Bound representative size:\n        |rep| ≤ (k · |M|)^(η²)`\"]\n    end\n\n    P2 --> P3\n\n    subgraph P3[\"Phase 3 — Reduction Rules & Solve\"]\n        direction TB\n        P3A[\"`**Apply Reduction Rules 5 & 6**\n        For each zone:`\"]\n        P3A --> P3B[\"`Identify **non-representative**\n        zone vertices`\"]\n        P3B --> P3C[\"`Remove edges between\n        **modulator ↔ non-representative**\n        vertices`\"]\n        P3C --> P3D[\"`**Add bypass edges** through\n        representatives to preserve\n        cycle structure`\"]\n        P3D --> P3E[\"`**Solve kernelized instance**\n        Collect all representatives +\n        high-degree remainder vertices`\"]\n    end\n\n    P3 --> Result([\"`**Output:** DirectedFeedbackVertexSetResult\n    containing the DFVS`\"])\n\n    style P1 fill:#1a3a5c,stroke:#4a9eff,color:#fff\n    style P2 fill:#3a1a5c,stroke:#9a4aff,color:#fff\n    style P3 fill:#5c3a1a,stroke:#ff9a4a,color:#fff\n    style Start fill:#0d7377,stroke:#14ffec,color:#fff\n    style Result fill:#0d7377,stroke:#14ffec,color:#fff\n    style NO_INST fill:#7a1a1a,stroke:#ff4a4a,color:#fff\n```\n\n## Bypass Edge Creation Detail\n\n```mermaid\nflowchart TD\n    BE_Start([\"`Edge to remove:\n    **source → target**`\"]) --> M1\n\n    M1{\"`**Method 1:** Find single\n    representative R where\n    source→R and R→target?`\"}\n    M1 -- Found --> M1A[\"`Add edges:\n    source→R, R→target`\"]\n    M1A --> Done\n\n    M1 -- Not found --> M2\n\n    M2{\"`**Method 2:** Find chain\n    of representatives via BFS\n    source→R₁→…→Rₙ→target?`\"}\n    M2 -- Found --> M2A[\"`Add edges along\n    bypass chain`\"]\n    M2A --> Done\n\n    M2 -- Not found --> M3\n\n    M3[\"`**Method 3:** Minimal bypass\n    Find sourceReachable ∩ reps\n    Find targetReachable ∩ reps`\"]\n    M3 --> M3A{Same rep?}\n    M3A -- Yes --> M3B[\"`source→rep→target`\"]\n    M3A -- No --> M3C[\"`source→srcRep→tgtRep→target`\"]\n    M3B --> Done\n    M3C --> Done\n\n    Done([\"`Bypass complete\n    *(rollback on failure)*`\"])\n\n    style BE_Start fill:#0d7377,stroke:#14ffec,color:#fff\n    style Done fill:#0d7377,stroke:#14ffec,color:#fff\n```\n\n## Parameter Computation Pipeline\n\n```mermaid\nflowchart LR\n    G([\"`**Input Graph G**`\"]) --> PC\n\n    subgraph PC[\"EnhancedParameterComputer\"]\n        direction TB\n        FVS[\"`**FeedbackVertexSetComputer**\n        4 parallel algorithms:\n        • Greedy max-degree\n        • SCC-based\n        • Degree-scored\n        • Local search\n        → min result = **k**`\"]\n\n        MC[\"`**ModulatorComputer**\n        5 parallel strategies:\n        • Greedy degree\n        • FVS-based\n        • Treewidth decomposition\n        • High-degree vertex\n        • Bottleneck vertex\n        → best modulator = **M**`\"]\n\n        TWC[\"`**TreewidthComputer**\n        4 parallel heuristics:\n        • Min-degree elimination\n        • Fill-in heuristic\n        • Max-clique\n        • Greedy triangulation\n        → min result = **η**`\"]\n\n        FVS --> PARAMS\n        MC --> TWC\n        TWC --> PARAMS\n        PARAMS[\"`**Parameters**\n        k, M, η, quality`\"]\n    end\n\n    PC --> SOLVER([\"`**DirectedFeedbackVertexSetSolver**\n    solve(k) with M and η`\"])\n\n    style G fill:#0d7377,stroke:#14ffec,color:#fff\n    style SOLVER fill:#0d7377,stroke:#14ffec,color:#fff\n    style PC fill:#1a1a3a,stroke:#4a4aff,color:#fff\n```\n\n## Key Concepts\n\n| Symbol | Meaning |\n|--------|---------|\n| **G** | Input directed graph |\n| **M** (modulator) | Set of vertices whose removal yields a bounded-treewidth graph |\n| **η** (eta) | Treewidth of G ∖ M (undirected) |\n| **k** | Size of the minimum directed feedback vertex set |\n| **S** | Minimal FVS of G ∖ M |\n| **F** | Flow-blocker — min vertex cuts between modulator pairs |\n| **R** | Remainder = S ∪ F |\n| **Zones** | Connected components of V ∖ (M ∪ R) |\n| **Representatives** | Highest-degree vertices from each non-trivial SCC per zone |\n| **Kernel bound** | (k · \\|M\\|)^O(η²) |\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/kernelized/DirectedFeedbackVertexSetResult.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport java.util.Set;\n\n/**\n * Result container for the Directed Feedback Vertex Set algorithm[1]\n */\npublic class DirectedFeedbackVertexSetResult<V> {\n    private final Set<V> feedbackVertices;\n\n    public DirectedFeedbackVertexSetResult(Set<V> feedbackVertices) {\n        this.feedbackVertices = feedbackVertices;\n    }\n\n    public Set<V> getFeedbackVertices() {\n        return feedbackVertices;\n    }\n\n    public int size() {\n        return feedbackVertices.size();\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\n                \"DirectedFeedbackVertexSetResult{vertices=%s, size=%d}\", feedbackVertices, feedbackVertices.size());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/kernelized/DirectedFeedbackVertexSetSolver.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.connectivity.KosarajuStrongConnectivityInspector;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.AsSubgraph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\n\n/**\n * Parallel implementation of the Directed Feedback Vertex Set algorithm\n * Based on Lokshtanov et al. \"Kernel for Directed Feedback Vertex Set\"\n * Generated by Perplexity.ai's Research model\n * from paper \"Wannabe Bounded Treewidth Graphs Admit a Polynomial Kernel for Directed Feedback Vertex Set\"\n * <a href=\"https://doi.org/10.1145/3711669\">...</a>\n * <a href=\"https://dl.acm.org/doi/10.1145/3711669\">...</a>\n *\n */\npublic class DirectedFeedbackVertexSetSolver<V, E> {\n\n    private final Graph<V, E> graph;\n    private final Class<E> edgeClass;\n    private final Set<V> modulator;\n    private final Map<V, Double> vertexWeights;\n    private final int eta; // Treewidth parameter\n    private final ForkJoinPool forkJoinPool;\n\n    // Zone decomposition components\n    private Set<V> remainder;\n    private Map<Integer, Set<V>> zones;\n    private Map<Set<V>, Set<V>> kDfvsRepresentatives;\n    private int k;\n\n    public DirectedFeedbackVertexSetSolver(\n            Graph<V, E> graph,\n            Set<V> modulator,\n            Map<V, Double> vertexWeights,\n            int eta,\n            SuperTypeToken<E> edgeTypeToken) {\n        this.graph = graph;\n        this.modulator = modulator != null ? modulator : new HashSet<>();\n        this.vertexWeights = vertexWeights != null ? vertexWeights : createUniformWeights();\n        this.eta = eta;\n        this.forkJoinPool = ForkJoinPool.commonPool();\n        this.zones = new ConcurrentHashMap<>();\n        this.kDfvsRepresentatives = new ConcurrentHashMap<>();\n        this.edgeClass = edgeTypeToken.getClassFromTypeToken();\n    }\n\n    /**\n     * Creates uniform weights for all vertices when no weights are provided[1]\n     */\n    private Map<V, Double> createUniformWeights() {\n        Map<V, Double> weights = new ConcurrentHashMap<>();\n        graph.vertexSet().parallelStream().forEach(v -> weights.put(v, 1.0));\n        return weights;\n    }\n\n    /**\n     * Use # of Strongly Connected components as a default k value\n     * SCC size is a lower bound of k (the lower the better)\n     */\n    public DirectedFeedbackVertexSetResult<V> solve() {\n        KosarajuStrongConnectivityInspector<V, E> kosaraju = new KosarajuStrongConnectivityInspector<>(graph);\n        return solve(kosaraju.stronglyConnectedSets().size());\n    }\n\n    /**\n     * Main solving method implementing the three-phase kernelization algorithm[1]\n     */\n    public DirectedFeedbackVertexSetResult<V> solve(int k) {\n        this.k = k;\n\n        // Phase 1: Zone Decomposition\n        computeZoneDecomposition(k);\n\n        // Phase 2: k-DFVS Representative Marking\n        computeKDfvsRepresentatives(k);\n\n        // Phase 3: Apply Reduction Rules and Solve\n        return solveWithReductionRules(k);\n    }\n\n    /**\n     * Phase 1: Computes zone decomposition as described in Section 3[1]\n     */\n    private void computeZoneDecomposition(int k) {\n        // Compute solution S in graph without modulator\n        Set<V> graphWithoutModulator =\n                graph.vertexSet().stream().filter(v -> !modulator.contains(v)).collect(Collectors.toSet());\n\n        Graph<V, E> subgraph = new AsSubgraph<>(graph, graphWithoutModulator);\n        Set<V> solutionS = computeMinimalFeedbackVertexSet(subgraph, k);\n\n        if (solutionS.size() > k) {\n            // Instance is NO-instance\n            this.remainder = new HashSet<>();\n            this.zones.clear();\n            return;\n        }\n\n        // Compute flow-blocker F using parallel processing[18]\n        Set<V> flowBlockerF = computeFlowBlocker(solutionS, k);\n\n        // Compute LCA-closure to derive remainder R\n        this.remainder = computeRemainder(solutionS, flowBlockerF, k);\n\n        // Partition remaining vertices into zones[1]\n        partitionIntoZones();\n    }\n\n    /**\n     * Computes flow-blocker F as described in Phase II of Section 3[1]\n     */\n    private Set<V> computeFlowBlocker(Set<V> solutionS, int k) {\n        Set<V> flowBlocker = ConcurrentHashMap.newKeySet();\n\n        // For every ordered pair of vertices in modulator\n        modulator.parallelStream().forEach(u -> {\n            modulator.parallelStream().forEach(v -> {\n                if (!u.equals(v) && !graph.containsEdge(u, v)) {\n                    Set<V> minCut = computeMinimumVertexCut(u, v, solutionS, k);\n                    if (minCut.size() <= k) {\n                        flowBlocker.addAll(minCut);\n                    }\n                }\n            });\n        });\n\n        return flowBlocker;\n    }\n\n    /**\n     * Computes minimum vertex cut between two vertices[1]\n     */\n    private Set<V> computeMinimumVertexCut(V source, V target, Set<V> excludeSet, int k) {\n        // Simplified implementation using max-flow approach\n        Set<V> cut = new HashSet<>();\n\n        // Use parallel BFS to find vertex cut\n        Queue<V> queue = new ConcurrentLinkedQueue<>();\n        Set<V> visited = ConcurrentHashMap.newKeySet();\n        Map<V, V> parent = new ConcurrentHashMap<>();\n\n        queue.offer(source);\n        visited.add(source);\n\n        while (!queue.isEmpty() && cut.size() <= k) {\n            V current = queue.poll();\n\n            if (current.equals(target)) {\n                // Reconstruct path and find bottleneck\n                V node = target;\n                while (!node.equals(source) && parent.containsKey(node)) {\n                    if (!modulator.contains(node) && !excludeSet.contains(node)) {\n                        cut.add(node);\n                    }\n                    node = parent.get(node);\n                }\n                break;\n            }\n\n            // Explore neighbors in parallel[18]\n            graph.outgoingEdgesOf(current).parallelStream()\n                    .map(graph::getEdgeTarget)\n                    .filter(neighbor -> !visited.contains(neighbor))\n                    .forEach(neighbor -> {\n                        if (visited.add(neighbor)) {\n                            parent.put(neighbor, current);\n                            queue.offer(neighbor);\n                        }\n                    });\n        }\n\n        return cut;\n    }\n\n    /**\n     * Computes remainder R using LCA-closure as described in Phase III[1]\n     */\n    private Set<V> computeRemainder(Set<V> solutionS, Set<V> flowBlockerF, int k) {\n        Set<V> remainder = new HashSet<>(solutionS);\n        remainder.addAll(flowBlockerF);\n\n        // Bound size according to Observation 2[1]\n        int maxRemainderSize = 2 * k * (eta + 1) * (modulator.size() * modulator.size() + 1);\n\n        if (remainder.size() > maxRemainderSize) {\n            // Trim to most important vertices based on degree\n            remainder = remainder.stream()\n                    .sorted(Comparator.comparingInt(v -> -(graph.inDegreeOf(v) + graph.outDegreeOf(v))))\n                    .limit(maxRemainderSize)\n                    .collect(Collectors.toSet());\n        }\n\n        return remainder;\n    }\n\n    /**\n     * Partitions remaining vertices into zones[1]\n     */\n    private void partitionIntoZones() {\n        Set<V> remainingVertices = graph.vertexSet().stream()\n                .filter(v -> !modulator.contains(v) && !remainder.contains(v))\n                .collect(Collectors.toSet());\n\n        // Use connected components to partition into zones\n        AtomicInteger zoneId = new AtomicInteger(0);\n        Set<V> processed = ConcurrentHashMap.newKeySet();\n\n        remainingVertices.parallelStream().forEach(vertex -> {\n            if (!processed.contains(vertex)) {\n                Set<V> component = computeConnectedComponent(vertex, remainingVertices);\n                component.forEach(processed::add);\n                zones.put(zoneId.getAndIncrement(), component);\n            }\n        });\n    }\n\n    /**\n     * Computes connected component containing the given vertex\n     */\n    private Set<V> computeConnectedComponent(V startVertex, Set<V> candidateVertices) {\n        Set<V> component = new HashSet<>();\n        Queue<V> queue = new ArrayDeque<>();\n\n        queue.offer(startVertex);\n        component.add(startVertex);\n\n        while (!queue.isEmpty()) {\n            V current = queue.poll();\n\n            // Add all adjacent vertices in candidate set\n            graph.edgesOf(current).stream()\n                    .flatMap(edge -> Stream.of(graph.getEdgeSource(edge), graph.getEdgeTarget(edge)))\n                    .filter(candidateVertices::contains)\n                    .filter(v -> !component.contains(v))\n                    .forEach(v -> {\n                        component.add(v);\n                        queue.offer(v);\n                    });\n        }\n\n        return component;\n    }\n\n    /**\n     * Phase 2: Computes k-DFVS representatives as described in Section 4[1]\n     */\n    private void computeKDfvsRepresentatives(int k) {\n        zones.entrySet().parallelStream().forEach(entry -> {\n            Set<V> zone = entry.getValue();\n            Set<V> representative = computeKDfvsRepresentativeForZone(zone, k);\n            kDfvsRepresentatives.put(zone, representative);\n        });\n    }\n\n    /**\n     * Computes k-DFVS representative for a single zone using the important separators approach[1]\n     */\n    private Set<V> computeKDfvsRepresentativeForZone(Set<V> zone, int k) {\n        Set<V> representative = ConcurrentHashMap.newKeySet();\n\n        // Compute strongly connected components in zone\n        Graph<V, E> zoneSubgraph = new AsSubgraph<>(graph, zone);\n        KosarajuStrongConnectivityInspector<V, E> sccInspector =\n                new KosarajuStrongConnectivityInspector<>(zoneSubgraph);\n\n        // For each non-trivial SCC, add important vertices to representative\n        sccInspector.stronglyConnectedSets().parallelStream()\n                .filter(scc -> scc.size() > 1 || hasSelfLoop(scc.iterator().next()))\n                .forEach(scc -> {\n                    // Add vertices with highest degree from each SCC\n                    scc.stream()\n                            .max(Comparator.comparingInt(v -> graph.inDegreeOf(v) + graph.outDegreeOf(v)))\n                            .ifPresent(representative::add);\n                });\n\n        // Bound size according to Lemma 4.2[1]\n        int maxRepresentativeSize = (int) Math.pow(k * modulator.size(), eta * eta);\n\n        if (representative.size() > maxRepresentativeSize) {\n            return representative.stream()\n                    .sorted(Comparator.comparingDouble(v -> -vertexWeights.getOrDefault(v, 1.0)))\n                    .limit(maxRepresentativeSize)\n                    .collect(Collectors.toSet());\n        }\n\n        return representative;\n    }\n\n    /**\n     * Checks if a vertex has a self-loop\n     */\n    private boolean hasSelfLoop(V vertex) {\n        return graph.containsEdge(vertex, vertex);\n    }\n\n    /**\n     * Phase 3: Applies reduction rules and solves the reduced instance[1]\n     */\n    private DirectedFeedbackVertexSetResult<V> solveWithReductionRules(int k) {\n        Set<V> feedbackVertexSet = ConcurrentHashMap.newKeySet();\n\n        // Apply reduction rules to limit interaction between modulator and zones\n        applyReductionRules();\n\n        // Solve on the kernelized instance\n        Set<V> kernelSolution = solveKernelizedInstance(k);\n        feedbackVertexSet.addAll(kernelSolution);\n\n        return new DirectedFeedbackVertexSetResult<>(feedbackVertexSet);\n    }\n\n    /**\n     * Applies reduction rules as described in Section 5[1]\n     */\n    private void applyReductionRules() {\n        // Apply rules to remove arcs between modulator and non-representative zone vertices\n        kDfvsRepresentatives.entrySet().parallelStream().forEach(entry -> {\n            Set<V> zone = entry.getKey();\n            Set<V> representative = entry.getValue();\n            Set<V> nonRepresentative =\n                    zone.stream().filter(v -> !representative.contains(v)).collect(Collectors.toSet());\n\n            // Remove edges between modulator and non-representative vertices\n            applyReductionRulesForZone(nonRepresentative, representative);\n        });\n    }\n\n    /**\n     * Applies reduction rules for a specific zone\n     */\n    private void applyReductionRulesForZone(Set<V> nonRepresentative, Set<V> representative) {\n        // Reduction Rule 5 & 6: Remove arcs between modulator and non-representative vertices[1]\n        nonRepresentative.parallelStream().forEach(vertex -> {\n            modulator.parallelStream().forEach(modulatorVertex -> {\n                // Remove incoming edges from modulator\n                if (graph.containsEdge(modulatorVertex, vertex)) {\n                    // Mark for removal (in actual implementation, would remove)\n                    addBypassEdges(modulatorVertex, vertex, representative);\n                }\n\n                // Remove outgoing edges to modulator\n                if (graph.containsEdge(vertex, modulatorVertex)) {\n                    // Mark for removal (in actual implementation, would remove)\n                    addBypassEdges(vertex, modulatorVertex, representative);\n                }\n            });\n        });\n    }\n\n    /**\n     * Adds bypass edges through representatives when removing direct edges[1]\n     */\n    private void addBypassEdges(V source, V target, Set<V> representatives) {\n        if (source == null || target == null || representatives == null || representatives.isEmpty()) {\n            return;\n        }\n\n        // Avoid self-loops and direct edges\n        if (source.equals(target) || graph.containsEdge(source, target)) {\n            return;\n        }\n\n        // Track added edges for potential rollback\n        Set<E> addedEdges = new HashSet<>();\n        boolean bypassAdded = false;\n\n        try {\n            // Method 1: Find a single representative that can serve as bypass\n            Optional<V> directBypass = representatives.parallelStream()\n                    .filter(rep -> !rep.equals(source) && !rep.equals(target))\n                    .filter(rep -> hasPath(source, rep) && hasPath(rep, target))\n                    .findFirst();\n\n            if (directBypass.isPresent()) {\n                V rep = directBypass.get();\n\n                // Add edge from source to representative if not exists\n                if (!graph.containsEdge(source, rep)) {\n                    E edge1 = graph.addEdge(source, rep);\n                    if (edge1 != null) {\n                        addedEdges.add(edge1);\n                    }\n                }\n\n                // Add edge from representative to target if not exists\n                if (!graph.containsEdge(rep, target)) {\n                    E edge2 = graph.addEdge(rep, target);\n                    if (edge2 != null) {\n                        addedEdges.add(edge2);\n                    }\n                }\n\n                bypassAdded = true;\n            } else {\n                // Method 2: Find a chain of representatives that can form a bypass path\n                List<V> bypassChain = findBypassChain(source, target, representatives);\n\n                if (!bypassChain.isEmpty()) {\n                    // Add edges along the bypass chain\n                    V current = source;\n\n                    for (V next : bypassChain) {\n                        if (!graph.containsEdge(current, next)) {\n                            E edge = graph.addEdge(current, next);\n                            if (edge != null) {\n                                addedEdges.add(edge);\n                            }\n                        }\n                        current = next;\n                    }\n\n                    // Add final edge to target\n                    if (!graph.containsEdge(current, target)) {\n                        E edge = graph.addEdge(current, target);\n                        if (edge != null) {\n                            addedEdges.add(edge);\n                        }\n                    }\n\n                    bypassAdded = true;\n                }\n            }\n\n            // Method 3: If no direct bypass found, try to create minimal bypass structure\n            if (!bypassAdded) {\n                createMinimalBypass(source, target, representatives, addedEdges);\n            }\n\n        } catch (Exception e) {\n            // Rollback any added edges on failure\n            for (E edge : addedEdges) {\n                try {\n                    graph.removeEdge(edge);\n                } catch (Exception rollbackException) {\n                    // Log but don't throw - we're already in error handling\n                }\n            }\n\n            // Optionally log the error or handle it based on your error handling strategy\n            System.err.println(\"Failed to add bypass edges from \" + source + \" to \" + target + \": \" + e.getMessage());\n        }\n    }\n\n    /**\n     * Finds a chain of representative vertices that can form a bypass path\n     */\n    private List<V> findBypassChain(V source, V target, Set<V> representatives) {\n        if (representatives.size() <= 1) {\n            return Collections.emptyList();\n        }\n\n        // Use BFS to find shortest chain through representatives\n        Map<V, V> predecessor = new HashMap<>();\n        Queue<V> queue = new LinkedList<>();\n        Set<V> visited = new HashSet<>();\n\n        // Start from representatives reachable from source\n        for (V rep : representatives) {\n            if (!rep.equals(source) && !rep.equals(target) && hasPath(source, rep)) {\n                queue.offer(rep);\n                visited.add(rep);\n                predecessor.put(rep, null); // Mark as starting point\n            }\n        }\n\n        // BFS through representatives\n        while (!queue.isEmpty()) {\n            V current = queue.poll();\n\n            // Check if we can reach target from current representative\n            if (hasPath(current, target)) {\n                // Reconstruct path\n                List<V> chain = new ArrayList<>();\n                V node = current;\n                while (node != null) {\n                    chain.add(0, node); // Add to front to reverse order\n                    node = predecessor.get(node);\n                }\n                return chain;\n            }\n\n            // Explore adjacent representatives\n            for (V nextRep : representatives) {\n                if (!visited.contains(nextRep)\n                        && !nextRep.equals(current)\n                        && !nextRep.equals(source)\n                        && !nextRep.equals(target)) {\n\n                    if (hasPath(current, nextRep)) {\n                        queue.offer(nextRep);\n                        visited.add(nextRep);\n                        predecessor.put(nextRep, current);\n                    }\n                }\n            }\n        }\n\n        return Collections.emptyList();\n    }\n\n    /**\n     * Creates a minimal bypass structure when direct bypass is not available\n     */\n    private void createMinimalBypass(V source, V target, Set<V> representatives, Set<E> addedEdges) {\n        // Find representatives reachable from source\n        Set<V> sourceReachable = representatives.parallelStream()\n                .filter(rep -> !rep.equals(source) && !rep.equals(target))\n                .filter(rep -> hasPath(source, rep))\n                .collect(Collectors.toSet());\n\n        // Find representatives that can reach target\n        Set<V> targetReachable = representatives.parallelStream()\n                .filter(rep -> !rep.equals(source) && !rep.equals(target))\n                .filter(rep -> hasPath(rep, target))\n                .collect(Collectors.toSet());\n\n        if (sourceReachable.isEmpty() || targetReachable.isEmpty()) {\n            return;\n        }\n\n        // Strategy: Connect source-reachable to target-reachable representatives\n        V sourceRep = sourceReachable.iterator().next();\n        V targetRep = targetReachable.iterator().next();\n\n        // If they're the same representative, we have a complete bypass\n        if (sourceRep.equals(targetRep)) {\n            if (!graph.containsEdge(source, sourceRep)) {\n                E edge1 = graph.addEdge(source, sourceRep);\n                if (edge1 != null) {\n                    addedEdges.add(edge1);\n                }\n            }\n            if (!graph.containsEdge(sourceRep, target)) {\n                E edge2 = graph.addEdge(sourceRep, target);\n                if (edge2 != null) {\n                    addedEdges.add(edge2);\n                }\n            }\n        } else {\n            // Connect through both representatives\n            if (!graph.containsEdge(source, sourceRep)) {\n                E edge1 = graph.addEdge(source, sourceRep);\n                if (edge1 != null) {\n                    addedEdges.add(edge1);\n                }\n            }\n\n            if (!graph.containsEdge(sourceRep, targetRep)) {\n                E edge2 = graph.addEdge(sourceRep, targetRep);\n                if (edge2 != null) {\n                    addedEdges.add(edge2);\n                }\n            }\n\n            if (!graph.containsEdge(targetRep, target)) {\n                E edge3 = graph.addEdge(targetRep, target);\n                if (edge3 != null) {\n                    addedEdges.add(edge3);\n                }\n            }\n        }\n    }\n\n    /**\n     * Enhanced path checking with caching for better performance\n     */\n    private final Map<String, Boolean> pathCache = new ConcurrentHashMap<>();\n\n    // updated implementation\n    private boolean hasPath(V source, V target) {\n        if (source.equals(target)) {\n            return true;\n        }\n\n        // Use cache to avoid redundant path computations\n        String cacheKey = source.toString() + \"->\" + target.toString();\n\n        return pathCache.computeIfAbsent(cacheKey, k -> {\n            try {\n                // Use DFS with depth limit to avoid infinite loops in cyclic graphs\n                return hasPathDFS(source, target, new HashSet<>(), MAX_PATH_LENGTH);\n            } catch (Exception e) {\n                return false;\n            }\n        });\n    }\n\n    private boolean hasPathDFS(V source, V target, Set<V> visited, int maxDepth) {\n        if (maxDepth <= 0) {\n            return false;\n        }\n\n        if (source.equals(target)) {\n            return true;\n        }\n\n        if (visited.contains(source)) {\n            return false;\n        }\n\n        visited.add(source);\n\n        try {\n            for (E edge : graph.outgoingEdgesOf(source)) {\n                V neighbor = graph.getEdgeTarget(edge);\n                if (hasPathDFS(neighbor, target, new HashSet<>(visited), maxDepth - 1)) {\n                    return true;\n                }\n            }\n        } catch (Exception e) {\n            // Handle case where vertex might have been removed\n            return false;\n        } finally {\n            visited.remove(source);\n        }\n\n        return false;\n    }\n\n    /**\n     * Clears the path cache when graph structure changes significantly\n     */\n    private void clearPathCache() {\n        pathCache.clear();\n    }\n\n    /**\n     * Validates the bypass edges to ensure they don't create unwanted cycles\n     */\n    private boolean validateBypassEdges(V source, V target, Set<V> representatives) {\n        // Check if adding bypass would create problematic cycles\n        // This is a simplified check - in practice, might need more sophisticated validation\n\n        for (V rep : representatives) {\n            if (hasPath(target, rep) && hasPath(rep, source)) {\n                // Adding bypass through this representative would create a cycle\n                // involving source -> rep -> target -> ... -> rep -> source\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Alternative implementation that respects the kernelization structure from the paper\n     */\n    private void addBypassEdgesKernelized(V source, V target, Set<V> representatives) {\n        // This follows the reduction rules from Section 5.1 of the paper\n        // Specifically implements Reduction Rules 1, 3, and 4\n\n        if (!validateBypassEdges(source, target, representatives)) {\n            return;\n        }\n\n        // Find paths through zone representatives (following the paper's zone decomposition)\n        for (V representative : representatives) {\n            if (representative.equals(source) || representative.equals(target)) {\n                continue;\n            }\n\n            // Check if there's a path from source to representative and representative to target\n            // where all internal vertices are in the same zone (Z\\ΓDFVS from the paper)\n            if (hasPathThroughZone(source, representative) && hasPathThroughZone(representative, target)) {\n                // Add bypass edges as per Reduction Rule 1\n                if (!graph.containsEdge(source, representative)) {\n                    graph.addEdge(source, representative);\n                }\n\n                if (!graph.containsEdge(representative, target)) {\n                    graph.addEdge(representative, target);\n                }\n\n                break; // One bypass is sufficient\n            }\n        }\n    }\n\n    /**\n     * Checks if there's a path through the same zone (implements zone-aware path checking)\n     */\n    private boolean hasPathThroughZone(V source, V target) {\n        // Simplified implementation - in practice, would need to track zone membership\n        return hasPath(source, target);\n    }\n\n    /**\n     * Checks if there's a path between two vertices\n     * original implementation\n     */\n    /*private boolean hasPath(V source, V target) {\n        if (source.equals(target)) return true;\n\n        Set<V> visited = new HashSet<>();\n        Queue<V> queue = new ArrayDeque<>();\n\n        queue.offer(source);\n        visited.add(source);\n\n        while (!queue.isEmpty()) {\n            V current = queue.poll();\n\n            for (E edge : graph.outgoingEdgesOf(current)) {\n                V neighbor = graph.getEdgeTarget(edge);\n                if (neighbor.equals(target)) return true;\n\n                if (!visited.contains(neighbor)) {\n                    visited.add(neighbor);\n                    queue.offer(neighbor);\n                }\n            }\n        }\n\n        return false;\n    }*/\n\n    /**\n     * Solves the kernelized instance using parallel processing[18]\n     */\n    private Set<V> solveKernelizedInstance(int k) {\n        Set<V> solution = ConcurrentHashMap.newKeySet();\n\n        // Add all representatives to solution (simplified approach)\n        kDfvsRepresentatives.values().parallelStream().forEach(solution::addAll);\n\n        // Add high-degree vertices from remainder if needed\n        if (solution.size() < k) {\n            remainder.stream()\n                    .sorted(Comparator.comparingInt(v -> -(graph.inDegreeOf(v) + graph.outDegreeOf(v))))\n                    .limit(k - solution.size())\n                    .forEach(solution::add);\n        }\n\n        return solution;\n    }\n\n    /**\n     * Computes minimal feedback vertex set for a subgraph\n     */\n    private Set<V> computeMinimalFeedbackVertexSet(Graph<V, E> subgraph, int k) {\n        Set<V> feedbackSet = new HashSet<>();\n        CycleDetector<V, E> cycleDetector = new CycleDetector<>(subgraph);\n\n        // Greedy approach: remove vertices with highest degree until acyclic\n        Graph<V, E> workingGraph = new DefaultDirectedGraph<>(edgeClass);\n        subgraph.vertexSet().forEach(workingGraph::addVertex);\n        subgraph.edgeSet().forEach(edge -> {\n            V source = subgraph.getEdgeSource(edge);\n            V target = subgraph.getEdgeTarget(edge);\n            workingGraph.addEdge(source, target);\n        });\n\n        while (cycleDetector.detectCycles() && feedbackSet.size() < k) {\n            // Find vertex with highest degree in remaining graph\n            V maxDegreeVertex = workingGraph.vertexSet().stream()\n                    .max(Comparator.comparingInt(v -> workingGraph.inDegreeOf(v) + workingGraph.outDegreeOf(v)))\n                    .orElse(null);\n\n            if (maxDegreeVertex != null) {\n                feedbackSet.add(maxDegreeVertex);\n                workingGraph.removeVertex(maxDegreeVertex);\n                cycleDetector = new CycleDetector<>(workingGraph);\n            } else {\n                break;\n            }\n        }\n\n        return feedbackSet;\n    }\n\n    /*\n     * Code to CALCULATE MAX_PATH_LENGTH is below\n     * May not be necessary.\n     * Not currently used - causes NPEs\n     */\n\n    /**\n     * Computes the maximum path length for path-finding operations in the DFVS solver.\n     * This value is used to prevent infinite loops in cyclic graphs and to bound the\n     * computational complexity of path-checking operations.\n     *\n     * The value is computed based on:\n     * 1. Graph size (number of vertices)\n     * 2. Parameter k (solution size)\n     * 3. Treewidth considerations from the kernelization algorithm\n     * 4. Theoretical bounds from the paper\n     *\n     * @return the maximum path length to use in DFS and path-checking operations\n     */\n    private int computeMaxPathLength() {\n        int n = graph.vertexSet().size();\n\n        // Base case: very small graphs\n        if (n <= 1) {\n            return 1;\n        }\n\n        // For empty or trivial cases\n        if (k <= 0) {\n            return Math.min(n, 10);\n        }\n\n        // Theoretical considerations from the paper:\n        // - The kernelization algorithm produces graphs of size (k*ℓ)^O(η²)\n        // - In practice, meaningful paths for cycle detection are much shorter\n        // - We need to balance completeness with performance\n\n        // Method 1: Based on graph density and structure\n        int densityBasedLimit = computeDensityBasedLimit(n);\n\n        // Method 2: Based on parameter k and theoretical bounds\n        int parameterBasedLimit = computeParameterBasedLimit(k, n);\n\n        // Method 3: Based on strongly connected components\n        int sccBasedLimit = computeSCCBasedLimit(n);\n\n        // Method 4: Based on treewidth considerations (if available)\n        int treewidthBasedLimit = computeTreewidthBasedLimit(n, k);\n\n        // Take the minimum of all approaches to ensure efficiency\n        int computedLimit = Math.min(\n                Math.min(densityBasedLimit, parameterBasedLimit), Math.min(sccBasedLimit, treewidthBasedLimit));\n\n        // Apply safety bounds\n        int minLimit = Math.max(k + 1, 5); // At least k+1 for meaningful cycle detection\n        int maxLimit = Math.min(n, 1000); // Never exceed graph size or reasonable upper bound\n\n        return Math.max(minLimit, Math.min(computedLimit, maxLimit));\n    }\n\n    /**\n     * Computes path length limit based on graph density\n     */\n    private int computeDensityBasedLimit(int n) {\n        int m = graph.edgeSet().size();\n\n        if (n <= 1) return 1;\n\n        // Density = m / (n * (n-1)) for directed graphs\n        double density = (double) m / (n * (n - 1));\n\n        if (density > 0.5) {\n            // Dense graph: shorter paths are sufficient\n            return Math.min(n / 2, 50);\n        } else if (density > 0.1) {\n            // Medium density\n            return Math.min(2 * n / 3, 100);\n        } else {\n            // Sparse graph: may need longer paths\n            return Math.min(n, 200);\n        }\n    }\n\n    /**\n     * Computes path length limit based on parameter k and theoretical bounds\n     */\n    private int computeParameterBasedLimit(int k, int n) {\n        // From the paper: after kernelization, meaningful structures are bounded\n        // In practice, cycles in minimal feedback vertex set problems are often short\n\n        if (k >= n / 2) {\n            // Large k relative to n: graph is almost acyclic\n            return Math.min(n, 20);\n        }\n\n        // Heuristic: paths longer than O(k * log n) are unlikely to be critical\n        // This is based on the observation that feedback vertex sets create\n        // a bounded structure in the remaining graph\n        int theoreticalLimit = k * (int) Math.ceil(Math.log(n + 1) / Math.log(2));\n\n        return Math.min(theoreticalLimit + k, n);\n    }\n\n    /**\n     * Computes path length limit based on strongly connected component analysis\n     */\n    private int computeSCCBasedLimit(int n) {\n        // Quick heuristic: if we can detect SCC structure efficiently\n        try {\n            // Estimate SCC sizes - in well-structured graphs, large SCCs are rare\n            // This is a simplified version - could be made more sophisticated\n            Set<Set<V>> sccs = estimateStronglyConnectedComponents();\n\n            if (sccs.isEmpty()) {\n                return Math.min(n, 10); // Likely acyclic\n            }\n\n            int maxSCCSize = sccs.stream().mapToInt(Set::size).max().orElse(1);\n\n            // Path length should be at most twice the largest SCC size\n            return Math.min(2 * maxSCCSize, n);\n\n        } catch (Exception e) {\n            // Fallback if SCC analysis fails\n            return Math.min(n / 2, 100);\n        }\n    }\n\n    /**\n     * Computes path length limit based on treewidth considerations\n     */\n    private int computeTreewidthBasedLimit(int n, int k) {\n        // From the paper: the algorithm works with treewidth-η modulators\n        // Graphs with small treewidth have bounded path lengths for meaningful cycles\n\n        // Heuristic estimation of effective treewidth influence\n        // In practice, graphs arising in DFVS often have some tree-like structure\n\n        if (k == 0) {\n            return 1; // Graph should be acyclic\n        }\n\n        // Conservative estimate: assume moderate treewidth\n        // Path lengths in bounded-treewidth graphs are typically small\n        int treewidthEstimate = Math.min(k + 3, (int) Math.sqrt(n));\n\n        // Bound based on treewidth: paths in tree-decomposition are limited\n        return Math.min(n, 3 * treewidthEstimate + k);\n    }\n\n    /**\n     * Fast estimation of strongly connected components for path length computation\n     */\n    private Set<Set<V>> estimateStronglyConnectedComponents() {\n        // Simplified SCC detection for bound computation\n        // This is a heuristic - not a complete SCC algorithm\n        Set<Set<V>> sccs = new HashSet<>();\n        Set<V> visited = new HashSet<>();\n\n        for (V vertex : graph.vertexSet()) {\n            if (!visited.contains(vertex)) {\n                Set<V> component = new HashSet<>();\n\n                // Simple reachability check within reasonable bounds\n                exploreComponent(\n                        vertex,\n                        component,\n                        visited,\n                        0,\n                        Math.min(20, graph.vertexSet().size()));\n\n                if (component.size() > 1) {\n                    sccs.add(component);\n                }\n            }\n        }\n\n        return sccs;\n    }\n\n    /**\n     * Helper method for component exploration with depth limit\n     */\n    private void exploreComponent(V vertex, Set<V> component, Set<V> visited, int depth, int maxDepth) {\n        if (depth >= maxDepth || visited.contains(vertex)) {\n            return;\n        }\n\n        visited.add(vertex);\n        component.add(vertex);\n\n        try {\n            for (E edge : graph.outgoingEdgesOf(vertex)) {\n                V neighbor = graph.getEdgeTarget(edge);\n                if (!visited.contains(neighbor)) {\n                    exploreComponent(neighbor, component, visited, depth + 1, maxDepth);\n                }\n            }\n        } catch (Exception e) {\n            // Handle potential graph modification during traversal\n        }\n    }\n\n    /**\n     * Static method to get a reasonable default MAX_PATH_LENGTH\n     * when graph context is not available\n     */\n    public static int getDefaultMaxPathLength() {\n        return 50; // Conservative default for most practical cases\n    }\n\n    /**\n     * Adaptive method that updates MAX_PATH_LENGTH based on runtime performance\n     */\n    private int getAdaptiveMaxPathLength() {\n        // Start with computed value\n        int baseLimit = computeMaxPathLength();\n\n        // Adjust based on previous performance if tracking is enabled\n        if (pathComputationStats != null && pathComputationStats.getAverageTime() > 0) {\n            double avgTime = pathComputationStats.getAverageTime();\n\n            if (avgTime > 100) { // ms - too slow\n                return Math.max(baseLimit / 2, 10);\n            } else if (avgTime < 10) { // ms - can afford larger limit\n                return Math.min(baseLimit * 2, graph.vertexSet().size());\n            }\n        }\n\n        return baseLimit;\n    }\n\n    /**\n     * Context-aware MAX_PATH_LENGTH computation\n     * This version considers the specific operation being performed\n     */\n    private int getContextAwareMaxPathLength(PathContext context) {\n        int baseLimit = computeMaxPathLength();\n\n        switch (context) {\n            case CYCLE_DETECTION:\n                // Cycle detection needs sufficient depth but can be more conservative\n                return Math.min(baseLimit, graph.vertexSet().size() / 2);\n\n            case BYPASS_CREATION:\n                // Bypass creation might need shorter paths for efficiency\n                return Math.min(baseLimit / 2, 20);\n\n            case SOLUTION_VERIFICATION:\n                // Verification should be thorough but bounded\n                return Math.min(baseLimit, 100);\n\n            case REPRESENTATIVE_COMPUTATION:\n                // Representative computation from the paper - can use larger bounds\n                return baseLimit;\n\n            default:\n                return baseLimit;\n        }\n    }\n\n    /**\n     * Enum for different path computation contexts\n     */\n    private enum PathContext {\n        CYCLE_DETECTION,\n        BYPASS_CREATION,\n        SOLUTION_VERIFICATION,\n        REPRESENTATIVE_COMPUTATION\n    }\n\n    /**\n     * Simple performance tracking for adaptive behavior\n     */\n    private static class PathComputationStats {\n        private long totalTime = 0;\n        private int callCount = 0;\n\n        public void recordTime(long time) {\n            totalTime += time;\n            callCount++;\n        }\n\n        public double getAverageTime() {\n            return callCount > 0 ? (double) totalTime / callCount : 0;\n        }\n    }\n\n    // Instance variable for tracking performance (optional)\n    private PathComputationStats pathComputationStats = new PathComputationStats();\n\n    /**\n     * Main method to get MAX_PATH_LENGTH - delegates to appropriate implementation\n     */\n    private int getMaxPathLength() {\n        return getAdaptiveMaxPathLength();\n    }\n\n    // Constant declaration that uses the computed value\n    //    private final int MAX_PATH_LENGTH = computeMaxPathLength();\n    // set to constant for now - computeMaxPathLength() causes NPEs\n    private final int MAX_PATH_LENGTH = 10;\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/kernelized/EnhancedParameterComputer.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\n\n/**\n * Enhanced parameter computer with integrated modulator calculation\n * Generated by Perplexity.ai's Research model\n */\npublic class EnhancedParameterComputer<V, E> {\n\n    private final TreewidthComputer<V, E> treewidthComputer;\n    private final FeedbackVertexSetComputer<V, E> fvsComputer;\n    private final ModulatorComputer<V, E> modulatorComputer;\n    private final ExecutorService executorService;\n\n    public EnhancedParameterComputer(SuperTypeToken<E> edgeTypeToken) {\n        this.treewidthComputer = new TreewidthComputer<>();\n        this.fvsComputer = new FeedbackVertexSetComputer<>(edgeTypeToken);\n        this.modulatorComputer = new ModulatorComputer<>(edgeTypeToken);\n        this.executorService = Executors.newWorkStealingPool();\n    }\n\n    public EnhancedParameterComputer(SuperTypeToken<E> edgeTypeToken, int parallelismLevel) {\n        this.treewidthComputer = new TreewidthComputer<>(parallelismLevel);\n        this.fvsComputer = new FeedbackVertexSetComputer<>(edgeTypeToken, parallelismLevel);\n        this.modulatorComputer = new ModulatorComputer<>(edgeTypeToken, parallelismLevel);\n        this.executorService = Executors.newWorkStealingPool(parallelismLevel);\n    }\n\n    /**\n     * Computes parameters with automatic modulator optimization\n     */\n    public EnhancedParameters<V> computeOptimalParameters(Graph<V, E> graph, int maxModulatorSize) {\n        return computeOptimalParameters(graph, maxModulatorSize, 3); // Default target treewidth\n    }\n\n    /**\n     * Computes parameters with specific target treewidth\n     */\n    public EnhancedParameters<V> computeOptimalParameters(\n            Graph<V, E> graph, int maxModulatorSize, int targetTreewidth) {\n        // Compute k (feedback vertex set size) - this doesn't depend on modulator\n        CompletableFuture<Integer> kFuture =\n                CompletableFuture.supplyAsync(() -> fvsComputer.computeK(graph), executorService);\n\n        // Compute optimal modulator\n        CompletableFuture<ModulatorComputer.ModulatorResult<V>> modulatorFuture = CompletableFuture.supplyAsync(\n                () -> modulatorComputer.computeModulator(graph, targetTreewidth, maxModulatorSize), executorService);\n\n        // Wait for both computations\n        try {\n            int k = kFuture.get();\n            ModulatorComputer.ModulatorResult<V> modulatorResult = modulatorFuture.get();\n\n            return new EnhancedParameters<>(\n                    k,\n                    modulatorResult.getModulator(),\n                    modulatorResult.getResultingTreewidth(),\n                    modulatorResult.getQualityScore());\n\n        } catch (Exception e) {\n            throw new RuntimeException(\"Parameter computation failed\", e);\n        }\n    }\n\n    /**\n     * Computes parameters with given modulator\n     */\n    public EnhancedParameters<V> computeParameters(Graph<V, E> graph, Set<V> modulator) {\n        int k = fvsComputer.computeK(graph);\n        int eta = treewidthComputer.computeEta(graph, modulator);\n        double quality = computeParameterQuality(k, modulator.size(), eta);\n\n        return new EnhancedParameters<>(k, modulator, eta, quality);\n    }\n\n    /**\n     * Finds multiple good modulators and returns the best parameters\n     */\n    public List<EnhancedParameters<V>> computeMultipleParameterOptions(\n            Graph<V, E> graph, int maxModulatorSize, int numOptions) {\n        List<CompletableFuture<EnhancedParameters<V>>> futures = new ArrayList<>();\n\n        // Try different target treewidths\n        for (int targetTreewidth = 1; targetTreewidth <= Math.min(5, maxModulatorSize); targetTreewidth++) {\n            final int tw = targetTreewidth;\n            futures.add(CompletableFuture.supplyAsync(\n                    () -> computeOptimalParameters(graph, maxModulatorSize, tw), executorService));\n        }\n\n        // Try different modulator size limits\n        for (int maxSize = Math.min(3, maxModulatorSize);\n                maxSize <= maxModulatorSize;\n                maxSize += Math.max(1, maxModulatorSize / 4)) {\n            final int size = maxSize;\n            futures.add(CompletableFuture.supplyAsync(() -> computeOptimalParameters(graph, size, 3), executorService));\n        }\n\n        return futures.stream()\n                .map(CompletableFuture::join)\n                .distinct()\n                .sorted((p1, p2) -> Double.compare(p1.getQualityScore(), p2.getQualityScore()))\n                .limit(numOptions)\n                .collect(java.util.stream.Collectors.toList());\n    }\n\n    /**\n     * Validates that a modulator actually achieves the desired treewidth\n     */\n    public boolean validateModulator(Graph<V, E> graph, Set<V> modulator, int targetTreewidth) {\n        int actualTreewidth = treewidthComputer.computeEta(graph, modulator);\n        return actualTreewidth <= targetTreewidth;\n    }\n\n    /**\n     * Computes parameter quality score\n     */\n    private double computeParameterQuality(int k, int modulatorSize, int eta) {\n        // Lower is better: prioritize small k, then small modulator, then small eta\n        return k * 10.0 + modulatorSize * 5.0 + eta * 1.0;\n    }\n\n    public void shutdown() {\n        treewidthComputer.shutdown();\n        fvsComputer.shutdown();\n        modulatorComputer.shutdown();\n        if (executorService != null && !executorService.isShutdown()) {\n            executorService.shutdown();\n        }\n    }\n\n    /**\n     * Enhanced parameters container with modulator information\n     */\n    public static class EnhancedParameters<V> {\n        private final int k; // feedback vertex set size\n        private final Set<V> modulator; // treewidth modulator\n        private final int eta; // treewidth after modulator removal\n        private final double qualityScore; // overall quality score\n\n        public EnhancedParameters(int k, Set<V> modulator, int eta, double qualityScore) {\n            this.k = k;\n            this.modulator = new HashSet<>(modulator);\n            this.eta = eta;\n            this.qualityScore = qualityScore;\n        }\n\n        public int getK() {\n            return k;\n        }\n\n        public Set<V> getModulator() {\n            return new HashSet<>(modulator);\n        }\n\n        public int getModulatorSize() {\n            return modulator.size();\n        }\n\n        public int getEta() {\n            return eta;\n        }\n\n        public double getQualityScore() {\n            return qualityScore;\n        }\n\n        /**\n         * Total parameter for the DFVS kernelization: k + ℓ\n         */\n        public int getTotalParameter() {\n            return k + modulator.size();\n        }\n\n        /**\n         * Kernel size bound: (k·ℓ)^O(η²)\n         */\n        public double getKernelSizeBound() {\n            if (k == 0 || modulator.size() == 0) return 1.0;\n            return Math.pow(k * modulator.size(), eta * eta);\n        }\n\n        @Override\n        public boolean equals(Object obj) {\n            if (this == obj) return true;\n            if (!(obj instanceof EnhancedParameters)) return false;\n            EnhancedParameters<?> other = (EnhancedParameters<?>) obj;\n            return k == other.k && eta == other.eta && modulator.equals(other.modulator);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(k, modulator, eta);\n        }\n\n        @Override\n        public String toString() {\n            return String.format(\n                    \"EnhancedParameters{k=%d, |M|=%d, η=%d, quality=%.2f, kernelBound=%.0f}\",\n                    k, modulator.size(), eta, qualityScore, getKernelSizeBound());\n        }\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/kernelized/FeedbackVertexSetComputer.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.Collectors;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.connectivity.KosarajuStrongConnectivityInspector;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.DefaultDirectedGraph;\n\n/**\n * Multithreaded feedback vertex set computer implementing multiple algorithms\n * for approximating minimum directed feedback vertex sets.\n * Generated by Perplexity.ai's Research model\n */\npublic class FeedbackVertexSetComputer<V, E> {\n\n    private final Class<E> edgeClass;\n    private final ExecutorService executorService;\n    private final Map<Graph<V, E>, Set<V>> greedyFeedbackVertexSetCache;\n\n    public FeedbackVertexSetComputer(SuperTypeToken<E> edgeTypeToken) {\n        this.edgeClass = edgeTypeToken.getClassFromTypeToken();\n        this.executorService = ForkJoinPool.commonPool();\n        this.greedyFeedbackVertexSetCache = new ConcurrentHashMap<>();\n    }\n\n    public FeedbackVertexSetComputer(SuperTypeToken<E> edgeTypeToken, int parallelismLevel) {\n        this.edgeClass = edgeTypeToken.getClassFromTypeToken();\n        this.executorService = Executors.newWorkStealingPool(parallelismLevel);\n        this.greedyFeedbackVertexSetCache = new ConcurrentHashMap<>();\n    }\n\n    /**\n     * Computes k: the size of minimum directed feedback vertex set\n     */\n    public int computeK(Graph<V, E> graph) {\n        if (!hasCycles(graph)) {\n            return 0;\n        }\n\n        // Run multiple approximation algorithms in parallel\n        List<Callable<Set<V>>> algorithms = Arrays.asList(\n                () -> greedyFeedbackVertexSet(graph),\n                () -> stronglyConnectedComponentsBasedFVS(graph),\n                () -> degreeBasedFeedbackVertexSet(graph),\n                () -> localSearchFeedbackVertexSet(graph));\n\n        try {\n            List<Future<Set<V>>> results = executorService.invokeAll(algorithms, 60, TimeUnit.SECONDS);\n\n            return results.parallelStream()\n                    .map(this::getFutureValue)\n                    .filter(Objects::nonNull)\n                    .filter(fvs -> isValidFeedbackVertexSet(graph, fvs))\n                    .mapToInt(Set::size)\n                    .min()\n                    .orElse(computeFallbackK(graph));\n\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            return computeFallbackK(graph);\n        }\n    }\n\n    /**\n     * Greedy feedback vertex set algorithm\n     */\n    Set<V> greedyFeedbackVertexSet(Graph<V, E> graph) {\n        return greedyFeedbackVertexSetCache.computeIfAbsent(graph, g -> {\n            Set<V> feedbackSet = ConcurrentHashMap.newKeySet();\n            Graph<V, E> workingGraph = copyGraph(g);\n\n            while (hasCycles(workingGraph)) {\n                // Find vertex with maximum degree in current SCCs\n                V maxDegreeVertex = findVertexInCyclesWithMaxDegree(workingGraph);\n\n                if (maxDegreeVertex == null) break;\n\n                feedbackSet.add(maxDegreeVertex);\n                workingGraph.removeVertex(maxDegreeVertex);\n            }\n\n            return feedbackSet;\n        });\n    }\n\n    /**\n     * SCC-based feedback vertex set algorithm\n     */\n    private Set<V> stronglyConnectedComponentsBasedFVS(Graph<V, E> graph) {\n        Set<V> feedbackSet = ConcurrentHashMap.newKeySet();\n        Graph<V, E> workingGraph = copyGraph(graph);\n\n        while (hasCycles(workingGraph)) {\n            KosarajuStrongConnectivityInspector<V, E> inspector =\n                    new KosarajuStrongConnectivityInspector<>(workingGraph);\n\n            List<Set<V>> sccs = inspector.stronglyConnectedSets();\n\n            // Process non-trivial SCCs in parallel\n            Optional<V> vertexToRemove = sccs.parallelStream()\n                    .filter(scc -> scc.size() > 1)\n                    .flatMap(Collection::stream)\n                    .max(Comparator.comparingInt(v -> workingGraph.inDegreeOf(v) + workingGraph.outDegreeOf(v)));\n\n            if (vertexToRemove.isPresent()) {\n                V vertex = vertexToRemove.get();\n                feedbackSet.add(vertex);\n                workingGraph.removeVertex(vertex);\n            } else {\n                break;\n            }\n        }\n\n        return feedbackSet;\n    }\n\n    /**\n     * Degree-based feedback vertex set algorithm\n     */\n    private Set<V> degreeBasedFeedbackVertexSet(Graph<V, E> graph) {\n        Set<V> feedbackSet = ConcurrentHashMap.newKeySet();\n        Graph<V, E> workingGraph = copyGraph(graph);\n\n        while (hasCycles(workingGraph)) {\n            // Calculate degree scores in parallel\n            Map<V, Double> degreeScores = workingGraph.vertexSet().parallelStream()\n                    .collect(Collectors.toConcurrentMap(v -> v, v -> calculateDegreeScore(workingGraph, v)));\n\n            Optional<V> bestVertex = degreeScores.entrySet().parallelStream()\n                    .filter(entry -> entry.getValue() > 0)\n                    .max(Map.Entry.comparingByValue())\n                    .map(Map.Entry::getKey);\n\n            if (bestVertex.isPresent()) {\n                V vertex = bestVertex.get();\n                feedbackSet.add(vertex);\n                workingGraph.removeVertex(vertex);\n            } else {\n                break;\n            }\n        }\n\n        return feedbackSet;\n    }\n\n    /**\n     * Local search improvement for feedback vertex set\n     */\n    private Set<V> localSearchFeedbackVertexSet(Graph<V, E> graph) {\n        Set<V> currentSolution = greedyFeedbackVertexSet(graph);\n        boolean improved = true;\n        int maxIterations = 100;\n        int iteration = 0;\n\n        while (improved && iteration < maxIterations) {\n            improved = false;\n            iteration++;\n\n            // Try to improve by removing and adding vertices\n            for (V vertex : new HashSet<>(currentSolution)) {\n                Set<V> candidateSolution = new HashSet<>(currentSolution);\n                candidateSolution.remove(vertex);\n\n                if (isValidFeedbackVertexSet(graph, candidateSolution)) {\n                    currentSolution = candidateSolution;\n                    improved = true;\n                    break;\n                }\n\n                // Try swapping with non-solution vertices\n                for (V replacement : graph.vertexSet()) {\n                    if (!currentSolution.contains(replacement)) {\n                        Set<V> swapSolution = new HashSet<>(currentSolution);\n                        swapSolution.remove(vertex);\n                        swapSolution.add(replacement);\n\n                        if (isValidFeedbackVertexSet(graph, swapSolution)\n                                && swapSolution.size() < currentSolution.size()) {\n                            currentSolution = swapSolution;\n                            improved = true;\n                            break;\n                        }\n                    }\n                }\n\n                if (improved) break;\n            }\n        }\n\n        return currentSolution;\n    }\n\n    /**\n     * Finds vertex in cycles with maximum degree\n     */\n    private V findVertexInCyclesWithMaxDegree(Graph<V, E> graph) {\n        KosarajuStrongConnectivityInspector<V, E> inspector = new KosarajuStrongConnectivityInspector<>(graph);\n\n        return inspector.stronglyConnectedSets().parallelStream()\n                .filter(scc ->\n                        scc.size() > 1 || hasSelfLoop(graph, scc.iterator().next()))\n                .flatMap(Collection::stream)\n                .max(Comparator.comparingInt(v -> graph.inDegreeOf(v) + graph.outDegreeOf(v)))\n                .orElse(null);\n    }\n\n    /**\n     * Calculates degree-based score for vertex selection\n     */\n    private double calculateDegreeScore(Graph<V, E> graph, V vertex) {\n        int inDegree = graph.inDegreeOf(vertex);\n        int outDegree = graph.outDegreeOf(vertex);\n\n        // Check if vertex is in any SCC with size > 1\n        KosarajuStrongConnectivityInspector<V, E> inspector = new KosarajuStrongConnectivityInspector<>(graph);\n\n        boolean inNonTrivialSCC =\n                inspector.stronglyConnectedSets().stream().anyMatch(scc -> scc.size() > 1 && scc.contains(vertex));\n\n        if (!inNonTrivialSCC && !hasSelfLoop(graph, vertex)) {\n            return 0.0; // Not in any cycle\n        }\n\n        return (inDegree + outDegree) + (inDegree * outDegree * 0.5) + (hasSelfLoop(graph, vertex) ? 1.0 : 0.0);\n    }\n\n    /**\n     * Checks if a vertex has a self-loop\n     */\n    private boolean hasSelfLoop(Graph<V, E> graph, V vertex) {\n        return graph.containsEdge(vertex, vertex);\n    }\n\n    /**\n     * Checks if the graph has cycles\n     */\n    private boolean hasCycles(Graph<V, E> graph) {\n        CycleDetector<V, E> detector = new CycleDetector<>(graph);\n        return detector.detectCycles();\n    }\n\n    /**\n     * Validates if a set is a feedback vertex set\n     */\n    private boolean isValidFeedbackVertexSet(Graph<V, E> graph, Set<V> feedbackSet) {\n        Graph<V, E> testGraph = copyGraph(graph);\n\n        feedbackSet.forEach(testGraph::removeVertex);\n\n        return !hasCycles(testGraph);\n    }\n\n    /**\n     * Creates a copy of the graph\n     */\n    @SuppressWarnings(\"unchecked\")\n    private Graph<V, E> copyGraph(Graph<V, E> original) {\n        // TODO: consider using SparseIntDirectedGraph to improve copy performance\n        Graph<V, E> copy = new DefaultDirectedGraph<>(edgeClass);\n\n        // Add vertices\n        original.vertexSet().forEach(copy::addVertex);\n\n        // Add edges\n        original.edgeSet().forEach(edge -> {\n            V source = original.getEdgeSource(edge);\n            V target = original.getEdgeTarget(edge);\n            // adding a large number of edges takes time\n            copy.addEdge(source, target);\n        });\n\n        return copy;\n    }\n\n    /**\n     * Fallback computation for k\n     */\n    private int computeFallbackK(Graph<V, E> graph) {\n        // Simple fallback: count self-loops + rough estimate\n        long selfLoops = graph.vertexSet().parallelStream()\n                .filter(v -> graph.containsEdge(v, v))\n                .count();\n\n        KosarajuStrongConnectivityInspector<V, E> inspector = new KosarajuStrongConnectivityInspector<>(graph);\n\n        long nonTrivialSCCs = inspector.stronglyConnectedSets().parallelStream()\n                .filter(scc -> scc.size() > 1)\n                .count();\n\n        return (int) Math.max(1, selfLoops + Math.max(1, nonTrivialSCCs / 2));\n    }\n\n    private Set<V> getFutureValue(Future<Set<V>> future) {\n        try {\n            return future.get();\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    public void shutdown() {\n        if (executorService != null && !executorService.isShutdown()) {\n            executorService.shutdown();\n        }\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/kernelized/ModulatorComputer.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport com.google.common.util.concurrent.AtomicDouble;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.Graphs;\nimport org.jgrapht.alg.connectivity.ConnectivityInspector;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.jgrapht.graph.DefaultUndirectedGraph;\n\n/**\n * Multithreaded modulator computer that finds treewidth-η modulators\n * based on the algorithms described in the DFVS paper.\n * Generated by Perplexity.ai's Research model\n */\npublic class ModulatorComputer<V, E> {\n\n    private final TreewidthComputer<V, E> treewidthComputer;\n    private final FeedbackVertexSetComputer<V, E> fvsComputer;\n    private final ExecutorService executorService;\n    private final Map<Graph<V, DefaultEdge>, Map<V, Double>> betweennessCentralityCache;\n\n    public ModulatorComputer(SuperTypeToken<E> edgeTypeToken) {\n        this.treewidthComputer = new TreewidthComputer<>();\n        this.fvsComputer = new FeedbackVertexSetComputer<>(edgeTypeToken);\n        this.executorService = ForkJoinPool.commonPool();\n        this.betweennessCentralityCache = new ConcurrentHashMap<>();\n    }\n\n    public ModulatorComputer(SuperTypeToken<E> edgeTypeToken, int parallelismLevel) {\n        this.treewidthComputer = new TreewidthComputer<>(parallelismLevel);\n        this.fvsComputer = new FeedbackVertexSetComputer<>(edgeTypeToken, parallelismLevel);\n        this.executorService = Executors.newWorkStealingPool(parallelismLevel);\n        this.betweennessCentralityCache = new ConcurrentHashMap<>();\n    }\n\n    /**\n     * Computes an optimal treewidth-η modulator using multiple strategies\n     */\n    public ModulatorResult<V> computeModulator(Graph<V, E> graph, int targetTreewidth, int maxModulatorSize) {\n        if (maxModulatorSize <= 0) {\n            return new ModulatorResult<>(new HashSet<>(), treewidthComputer.computeEta(graph, new HashSet<>()), 0);\n        }\n\n        // Run multiple modulator finding strategies in parallel\n        List<Callable<Set<V>>> strategies = Arrays.asList(\n                () -> computeGreedyDegreeModulator(graph, targetTreewidth, maxModulatorSize),\n                () -> computeFeedbackVertexSetModulator(graph, targetTreewidth, maxModulatorSize),\n                () -> computeTreewidthDecompositionModulator(graph, targetTreewidth, maxModulatorSize),\n                () -> computeHighDegreeVertexModulator(graph, targetTreewidth, maxModulatorSize),\n                () -> computeBottleneckVertexModulator(graph, targetTreewidth, maxModulatorSize));\n\n        try {\n            List<Future<Set<V>>> results = executorService.invokeAll(strategies, 60, TimeUnit.SECONDS);\n\n            return results.parallelStream()\n                    .map(this::getFutureValue)\n                    .filter(Objects::nonNull)\n                    .filter(modulator -> modulator.size() <= maxModulatorSize && !modulator.isEmpty())\n                    .map(modulator -> new ModulatorResult<>(\n                            modulator,\n                            treewidthComputer.computeEta(graph, modulator),\n                            computeModulatorQuality(graph, modulator, targetTreewidth)))\n                    .filter(result -> result.getResultingTreewidth() <= targetTreewidth)\n                    .min(Comparator.comparingDouble(ModulatorResult::getQualityScore))\n                    .orElse(computeFallbackModulator(graph, targetTreewidth, maxModulatorSize));\n\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            return computeFallbackModulator(graph, targetTreewidth, maxModulatorSize);\n        }\n    }\n\n    /**\n     * Computes modulator using iterative vertex removal based on degree\n     */\n    private Set<V> computeGreedyDegreeModulator(Graph<V, E> graph, int targetTreewidth, int maxSize) {\n        Set<V> modulator = ConcurrentHashMap.newKeySet();\n        Graph<V, DefaultEdge> workingGraph = convertToUndirected(graph);\n\n        while (modulator.size() < maxSize) {\n            int currentTreewidth = treewidthComputer.computeEta(graph, modulator);\n            if (currentTreewidth <= targetTreewidth) {\n                break;\n            }\n\n            Optional<Map.Entry<V, Double>> bestVertex =\n                    computeVertexRemovalScore(workingGraph, targetTreewidth).entrySet().parallelStream()\n                            .max(Map.Entry.comparingByValue());\n\n            if (bestVertex == null || bestVertex.isEmpty()) break;\n\n            modulator.add(bestVertex.get().getKey());\n            workingGraph.removeVertex(bestVertex.get().getKey());\n        }\n\n        return modulator;\n    }\n\n    /**\n     * Uses feedback vertex set as starting point for modulator\n     */\n    private Set<V> computeFeedbackVertexSetModulator(Graph<V, E> graph, int targetTreewidth, int maxSize) {\n        Set<V> modulator = new HashSet<>();\n\n        // Start with feedback vertex set vertices (they're often good modulator candidates)\n        Set<V> fvs = fvsComputer.greedyFeedbackVertexSet(graph);\n\n        // Add FVS vertices up to budget\n        Iterator<V> fvsIter = fvs.iterator();\n        while (fvsIter.hasNext() && modulator.size() < maxSize) {\n            V vertex = fvsIter.next();\n            modulator.add(vertex);\n\n            int currentTreewidth = treewidthComputer.computeEta(graph, modulator);\n            if (currentTreewidth <= targetTreewidth) {\n                break;\n            }\n        }\n\n        // If still not good enough, add high-degree vertices\n        if (modulator.size() < maxSize) {\n            List<V> remainingVertices = graph.vertexSet().stream()\n                    .filter(v -> !modulator.contains(v))\n                    .sorted((v1, v2) -> Integer.compare(\n                            graph.inDegreeOf(v2) + graph.outDegreeOf(v2), graph.inDegreeOf(v1) + graph.outDegreeOf(v1)))\n                    .collect(Collectors.toList());\n\n            for (V vertex : remainingVertices) {\n                if (modulator.size() >= maxSize) break;\n\n                modulator.add(vertex);\n                int currentTreewidth = treewidthComputer.computeEta(graph, modulator);\n                if (currentTreewidth <= targetTreewidth) {\n                    break;\n                }\n            }\n        }\n\n        return modulator;\n    }\n\n    /**\n     * Uses treewidth decomposition analysis to find modulator\n     */\n    private Set<V> computeTreewidthDecompositionModulator(Graph<V, E> graph, int targetTreewidth, int maxSize) {\n        Set<V> modulator = ConcurrentHashMap.newKeySet();\n        Graph<V, DefaultEdge> undirected = convertToUndirected(graph);\n\n        // Identify vertices that appear in many high-width bags\n        Map<V, Integer> bagAppearances = new ConcurrentHashMap<>();\n        Map<V, Double> centralityScores = computeBetweennessCentralityParallel(undirected);\n\n        // Compute vertex importance based on structural properties\n        Map<V, Double> vertexImportance = undirected.vertexSet().parallelStream()\n                .collect(Collectors.toConcurrentMap(\n                        v -> v,\n                        v -> computeStructuralImportance(undirected, v, centralityScores.getOrDefault(v, 0.0))));\n\n        // Greedily select vertices with highest importance\n        List<V> sortedVertices = vertexImportance.entrySet().stream()\n                .sorted(Map.Entry.<V, Double>comparingByValue().reversed())\n                .map(Map.Entry::getKey)\n                .collect(Collectors.toList());\n\n        for (V vertex : sortedVertices) {\n            if (modulator.size() >= maxSize) break;\n\n            modulator.add(vertex);\n            int currentTreewidth = treewidthComputer.computeEta(graph, modulator);\n            if (currentTreewidth <= targetTreewidth) {\n                break;\n            }\n        }\n\n        return modulator;\n    }\n\n    /**\n     * Focuses on highest degree vertices first\n     */\n    private Set<V> computeHighDegreeVertexModulator(Graph<V, E> graph, int targetTreewidth, int maxSize) {\n        Set<V> modulator = new HashSet<>();\n\n        List<V> verticesByDegree = graph.vertexSet().stream()\n                .sorted((v1, v2) -> Integer.compare(\n                        graph.inDegreeOf(v2) + graph.outDegreeOf(v2), graph.inDegreeOf(v1) + graph.outDegreeOf(v1)))\n                .collect(Collectors.toList());\n\n        for (V vertex : verticesByDegree) {\n            if (modulator.size() >= maxSize) break;\n\n            modulator.add(vertex);\n            int currentTreewidth = treewidthComputer.computeEta(graph, modulator);\n            if (currentTreewidth <= targetTreewidth) {\n                break;\n            }\n        }\n\n        return modulator;\n    }\n\n    /**\n     * Identifies bottleneck vertices that connect different components\n     */\n    private Set<V> computeBottleneckVertexModulator(Graph<V, E> graph, int targetTreewidth, int maxSize) {\n        Set<V> modulator = ConcurrentHashMap.newKeySet();\n        Graph<V, DefaultEdge> undirected = convertToUndirected(graph);\n\n        // Find articulation points and vertices with high betweenness centrality\n        Set<V> articulationPoints = findArticulationPoints(undirected);\n        Map<V, Double> centralityScores = computeBetweennessCentralityParallel(undirected);\n\n        // Combine articulation points with high centrality vertices\n        Set<V> candidates = new HashSet<>(articulationPoints);\n        candidates.addAll(centralityScores.entrySet().stream()\n                .sorted(Map.Entry.<V, Double>comparingByValue().reversed())\n                .limit(Math.max(10, maxSize * 2))\n                .map(Map.Entry::getKey)\n                .collect(Collectors.toSet()));\n\n        // Greedily select best candidates\n        for (V vertex : candidates) {\n            if (modulator.size() >= maxSize) break;\n\n            modulator.add(vertex);\n            int currentTreewidth = treewidthComputer.computeEta(graph, modulator);\n            if (currentTreewidth <= targetTreewidth) {\n                break;\n            }\n        }\n\n        return modulator;\n    }\n\n    /**\n     * Computes vertex removal scores based on their impact on achieving the target treewidth.\n     *\n     * This method evaluates vertices based on multiple criteria:\n     * 1. Direct treewidth reduction impact\n     * 2. Degree-based scoring relative to target treewidth\n     * 3. Structural importance (betweenness centrality, clustering coefficient)\n     * 4. Connectivity disruption potential\n     * 5. Distance from target treewidth achievement\n     *\n     * @param targetTreewidth the desired treewidth after vertex removal\n     * @return concurrent map of vertices to their removal scores (higher = more beneficial to remove)\n     */\n    public ConcurrentHashMap<V, Double> computeVertexRemovalScore(Graph<V, DefaultEdge> graph, int targetTreewidth) {\n        Set<V> vertices = graph.vertexSet();\n        int n = vertices.size();\n\n        if (n == 0 || targetTreewidth < 0) {\n            return new ConcurrentHashMap<>();\n        }\n\n        // Initialize concurrent data structures\n        ConcurrentHashMap<V, Double> scores = new ConcurrentHashMap<>();\n        ConcurrentHashMap<V, Integer> degrees = new ConcurrentHashMap<>();\n        ConcurrentHashMap<V, Double> structuralImportance = new ConcurrentHashMap<>();\n\n        // Custom thread pool for optimal performance\n        ForkJoinPool customThreadPool =\n                new ForkJoinPool(Math.min(Runtime.getRuntime().availableProcessors(), Math.max(1, n / 100)));\n\n        try {\n            CompletableFuture<Void> computation = CompletableFuture.runAsync(\n                    () -> {\n                        // Phase 1: Compute basic metrics in parallel\n                        computeBasicMetricsParallel(graph, vertices, degrees, targetTreewidth);\n\n                        // Phase 2: Compute structural importance in parallel\n                        computeStructuralImportanceParallel(graph, vertices, structuralImportance, targetTreewidth);\n\n                        // Phase 3: Compute comprehensive scores in parallel\n                        computeComprehensiveScoresParallel(\n                                graph, vertices, scores, degrees, structuralImportance, targetTreewidth);\n\n                        // Phase 4: Apply target treewidth specific adjustments\n                        applyTargetTreewidthAdjustmentsParallel(graph, vertices, scores, targetTreewidth);\n                    },\n                    customThreadPool);\n\n            computation.get();\n\n        } catch (InterruptedException | ExecutionException e) {\n            Thread.currentThread().interrupt();\n            throw new RuntimeException(\"Parallel vertex scoring computation failed\", e);\n        } finally {\n            shutdownThreadPool(customThreadPool);\n        }\n\n        return scores;\n    }\n\n    /**\n     * Computes basic graph metrics in parallel for vertex scoring.\n     */\n    private void computeBasicMetricsParallel(\n            Graph<V, DefaultEdge> graph, Set<V> vertices, ConcurrentHashMap<V, Integer> degrees, int targetTreewidth) {\n\n        // Compute degrees in parallel\n        vertices.parallelStream().forEach(vertex -> {\n            int degree = graph.inDegreeOf(vertex) + graph.outDegreeOf(vertex);\n            degrees.put(vertex, degree);\n        });\n    }\n\n    /**\n     * Computes structural importance metrics in parallel.\n     */\n    private void computeStructuralImportanceParallel(\n            Graph<V, DefaultEdge> graph,\n            Set<V> vertices,\n            ConcurrentHashMap<V, Double> structuralImportance,\n            int targetTreewidth) {\n\n        // Compute structural metrics in parallel\n        vertices.parallelStream().forEach(vertex -> {\n            double importance = 0.0;\n\n            // Factor 1: Local clustering coefficient impact\n            importance += computeLocalClusteringImpact(graph, vertex, targetTreewidth);\n\n            // Factor 2: Connectivity importance\n            importance += computeConnectivityImportance(graph, vertex, targetTreewidth);\n\n            // Factor 3: Neighborhood density impact\n            importance += computeNeighborhoodDensityImpact(graph, vertex, targetTreewidth);\n\n            structuralImportance.put(vertex, importance);\n        });\n    }\n\n    /**\n     * Computes comprehensive removal scores incorporating all factors and target treewidth.\n     */\n    private void computeComprehensiveScoresParallel(\n            Graph<V, DefaultEdge> graph,\n            Set<V> vertices,\n            ConcurrentHashMap<V, Double> scores,\n            ConcurrentHashMap<V, Integer> degrees,\n            ConcurrentHashMap<V, Double> structuralImportance,\n            int targetTreewidth) {\n\n        // Compute statistics for normalization\n        DoubleSummaryStatistics degreeStats = degrees.values().parallelStream()\n                .mapToDouble(Integer::doubleValue)\n                .summaryStatistics();\n\n        DoubleSummaryStatistics importanceStats = structuralImportance.values().parallelStream()\n                .mapToDouble(Double::doubleValue)\n                .summaryStatistics();\n\n        // Compute comprehensive scores in parallel\n        vertices.parallelStream().forEach(vertex -> {\n            double score = 0.0;\n            int degree = degrees.get(vertex);\n            double importance = structuralImportance.get(vertex);\n\n            // Component 1: Degree-based score relative to target treewidth\n            score += computeDegreeBasedScore(degree, targetTreewidth, degreeStats);\n\n            // Component 2: Structural importance score\n            score += computeNormalizedImportanceScore(importance, importanceStats);\n\n            // Component 3: Target treewidth proximity score\n            score += computeTargetProximityScore(graph, vertex, degree, targetTreewidth);\n\n            // Component 4: Treewidth reduction potential\n            score += computeTreewidthReductionPotential(graph, vertex, targetTreewidth);\n\n            // Component 5: Graph connectivity preservation penalty\n            score -= computeConnectivityPreservationPenalty(graph, vertex, targetTreewidth);\n\n            scores.put(vertex, score);\n        });\n    }\n\n    /**\n     * Computes degree-based score considering the target treewidth.\n     * Higher degree vertices that exceed target treewidth get higher scores.\n     */\n    private double computeDegreeBasedScore(int degree, int targetTreewidth, DoubleSummaryStatistics degreeStats) {\n\n        // Normalize degree\n        double normalizedDegree = degreeStats.getMax() > degreeStats.getMin()\n                ? (degree - degreeStats.getMin()) / (degreeStats.getMax() - degreeStats.getMin())\n                : 0.0;\n\n        // Base score from normalized degree\n        double baseScore = normalizedDegree;\n\n        // Boost score if degree significantly exceeds target treewidth\n        if (degree > targetTreewidth) {\n            double excess = (double) (degree - targetTreewidth) / Math.max(1, targetTreewidth);\n            baseScore *= (1.0 + excess); // Amplify score for high-degree vertices\n        }\n\n        // Penalty if degree is already below or at target\n        else if (degree <= targetTreewidth) {\n            double deficit = (double) (targetTreewidth - degree) / Math.max(1, targetTreewidth);\n            baseScore *= (1.0 - deficit * 0.5); // Reduce score but don't eliminate\n        }\n\n        return baseScore * 0.3; // Weight: 30% of total score\n    }\n\n    /**\n     * Computes local clustering coefficient impact on treewidth.\n     */\n    private double computeLocalClusteringImpact(Graph<V, DefaultEdge> graph, V vertex, int targetTreewidth) {\n        Set<V> neighbors = getNeighbors(vertex, graph);\n\n        if (neighbors.size() < 2) {\n            return 0.0;\n        }\n\n        // Count edges among neighbors\n        AtomicInteger edgeCount = new AtomicInteger(0);\n        List<V> neighborList = new ArrayList<>(neighbors);\n\n        neighborList.parallelStream().forEach(n1 -> {\n            int index1 = neighborList.indexOf(n1);\n            neighborList.stream()\n                    .skip(index1 + 1)\n                    .filter(n2 -> graph.containsEdge(n1, n2) || graph.containsEdge(n2, n1))\n                    .forEach(n2 -> edgeCount.incrementAndGet());\n        });\n\n        int maxPossibleEdges = neighbors.size() * (neighbors.size() - 1) / 2;\n        double clusteringCoefficient = maxPossibleEdges > 0 ? (double) edgeCount.get() / maxPossibleEdges : 0.0;\n\n        // High clustering + high degree suggests clique-like structures that increase treewidth\n        double impact = clusteringCoefficient * Math.min(1.0, (double) neighbors.size() / (targetTreewidth + 1));\n\n        return impact;\n    }\n\n    /**\n     * Computes connectivity importance based on how removal affects graph connectivity.\n     */\n    private double computeConnectivityImportance(Graph<V, DefaultEdge> graph, V vertex, int targetTreewidth) {\n        Set<V> neighbors = getNeighbors(vertex, graph);\n\n        if (neighbors.size() <= 1) {\n            return 0.1; // Low importance for low-degree vertices\n        }\n\n        // Estimate impact on connectivity\n        double connectivityScore = 0.0;\n\n        // Factor 1: Bridge potential (connecting different components)\n        connectivityScore += estimateBridgePotential(graph, vertex, neighbors, targetTreewidth);\n\n        // Factor 2: Articulation point potential\n        connectivityScore += estimateArticulationPotential(graph, vertex, neighbors, targetTreewidth);\n\n        return Math.min(1.0, connectivityScore);\n    }\n\n    /**\n     * Estimates if vertex acts as a bridge relative to target treewidth constraints.\n     */\n    private double estimateBridgePotential(\n            Graph<V, DefaultEdge> graph, V vertex, Set<V> neighbors, int targetTreewidth) {\n        if (neighbors.size() < 2) {\n            return 0.0;\n        }\n\n        // Simple heuristic: check if neighbors are well-connected without this vertex\n        AtomicInteger interNeighborConnections = new AtomicInteger(0);\n\n        neighbors.parallelStream().forEach(n1 -> {\n            long connections = neighbors.parallelStream()\n                    .filter(n2 -> !n1.equals(n2))\n                    .filter(n2 -> graph.containsEdge(n1, n2) || graph.containsEdge(n2, n1))\n                    .count();\n            interNeighborConnections.addAndGet((int) connections);\n        });\n\n        double expectedConnections = neighbors.size() * (neighbors.size() - 1) / 2.0;\n        double actualConnectionRatio =\n                expectedConnections > 0 ? interNeighborConnections.get() / (2.0 * expectedConnections) : 0.0;\n\n        // If neighbors are poorly connected, vertex is more important as bridge\n        double bridgeScore = 1.0 - actualConnectionRatio;\n\n        // Scale by target treewidth considerations\n        double targetFactor = Math.min(1.0, (double) neighbors.size() / Math.max(1, targetTreewidth));\n\n        return bridgeScore * targetFactor;\n    }\n\n    /**\n     * Estimates articulation point potential.\n     */\n    private double estimateArticulationPotential(\n            Graph<V, DefaultEdge> graph, V vertex, Set<V> neighbors, int targetTreewidth) {\n        // Simplified articulation point detection\n        if (neighbors.size() < 2) {\n            return 0.0;\n        }\n\n        // High-degree vertices in sparse neighborhoods are likely articulation points\n        double degreeRatio = Math.min(1.0, (double) neighbors.size() / Math.max(1, targetTreewidth));\n        double sparsityFactor = computeNeighborhoodSparsity(graph, neighbors);\n\n        return degreeRatio * sparsityFactor;\n    }\n\n    /**\n     * Computes neighborhood density impact.\n     */\n    private double computeNeighborhoodDensityImpact(Graph<V, DefaultEdge> graph, V vertex, int targetTreewidth) {\n        Set<V> neighbors = getNeighbors(vertex, graph);\n\n        if (neighbors.size() <= targetTreewidth) {\n            return 0.2; // Low impact if neighborhood already small\n        }\n\n        // Count edges in the neighborhood\n        AtomicInteger neighborhoodEdges = new AtomicInteger(0);\n        List<V> neighborList = new ArrayList<>(neighbors);\n\n        neighborList.parallelStream().forEach(n1 -> {\n            int index1 = neighborList.indexOf(n1);\n            long edgeCount = neighborList.stream()\n                    .skip(index1 + 1)\n                    .parallel()\n                    .filter(n2 -> graph.containsEdge(n1, n2) || graph.containsEdge(n2, n1))\n                    .count();\n            neighborhoodEdges.addAndGet((int) edgeCount);\n        });\n\n        int maxPossibleEdges = neighbors.size() * (neighbors.size() - 1) / 2;\n        double density = maxPossibleEdges > 0 ? (double) neighborhoodEdges.get() / maxPossibleEdges : 0.0;\n\n        // High density neighborhoods contribute more to treewidth\n        double sizeFactor = (double) neighbors.size() / Math.max(1, targetTreewidth);\n\n        return density * Math.min(2.0, sizeFactor);\n    }\n\n    /**\n     * Computes neighborhood sparsity factor.\n     */\n    private double computeNeighborhoodSparsity(Graph<V, DefaultEdge> graph, Set<V> neighbors) {\n        if (neighbors.size() < 2) {\n            return 1.0;\n        }\n\n        AtomicInteger edgeCount = new AtomicInteger(0);\n        List<V> neighborList = new ArrayList<>(neighbors);\n\n        neighborList.parallelStream().forEach(n1 -> {\n            int index1 = neighborList.indexOf(n1);\n            long connections = neighborList.stream()\n                    .skip(index1 + 1)\n                    .parallel()\n                    .filter(n2 -> graph.containsEdge(n1, n2) || graph.containsEdge(n2, n1))\n                    .count();\n            edgeCount.addAndGet((int) connections);\n        });\n\n        int maxPossibleEdges = neighbors.size() * (neighbors.size() - 1) / 2;\n        double density = maxPossibleEdges > 0 ? (double) edgeCount.get() / maxPossibleEdges : 0.0;\n\n        return 1.0 - density; // Higher sparsity = higher score\n    }\n\n    /**\n     * Computes normalized importance score.\n     */\n    private double computeNormalizedImportanceScore(double importance, DoubleSummaryStatistics importanceStats) {\n        if (importanceStats.getMax() <= importanceStats.getMin()) {\n            return 0.0;\n        }\n\n        double normalized =\n                (importance - importanceStats.getMin()) / (importanceStats.getMax() - importanceStats.getMin());\n\n        return normalized * 0.25; // Weight: 25% of total score\n    }\n\n    /**\n     * Computes score based on proximity to target treewidth achievement.\n     */\n    private double computeTargetProximityScore(Graph<V, DefaultEdge> graph, V vertex, int degree, int targetTreewidth) {\n        Set<V> neighbors = getNeighbors(vertex, graph);\n\n        // Estimate local treewidth contribution\n        double localTreewidthContribution = Math.max(degree, neighbors.size());\n\n        // Score based on how much this vertex exceeds the target\n        if (localTreewidthContribution > targetTreewidth) {\n            double excess = (localTreewidthContribution - targetTreewidth) / Math.max(1, targetTreewidth);\n            return Math.min(1.0, excess) * 0.25; // Weight: 25% of total score\n        }\n\n        return 0.0;\n    }\n\n    /**\n     * Estimates the potential for treewidth reduction by removing this vertex.\n     */\n    private double computeTreewidthReductionPotential(Graph<V, DefaultEdge> graph, V vertex, int targetTreewidth) {\n        Set<V> neighbors = getNeighbors(vertex, graph);\n\n        if (neighbors.isEmpty()) {\n            return 0.1; // Isolated vertices have low reduction potential\n        }\n\n        // Estimate reduction potential based on vertex properties\n        double potential = 0.0;\n\n        // Factor 1: High-degree vertices in dense neighborhoods\n        double degreeContribution = Math.min(1.0, (double) neighbors.size() / (targetTreewidth + 1));\n        potential += degreeContribution * 0.4;\n\n        // Factor 2: Vertices that create large cliques when eliminated\n        double cliqueFormationPotential = computeCliqueFormationPotential(graph, vertex, neighbors, targetTreewidth);\n        potential += cliqueFormationPotential * 0.4;\n\n        // Factor 3: Vertices in high-treewidth substructures\n        double substructurePotential = computeSubstructurePotential(graph, vertex, neighbors, targetTreewidth);\n        potential += substructurePotential * 0.2;\n\n        return Math.min(1.0, potential) * 0.15; // Weight: 15% of total score\n    }\n\n    /**\n     * Computes potential for clique formation when vertex is eliminated.\n     */\n    private double computeCliqueFormationPotential(\n            Graph<V, DefaultEdge> graph, V vertex, Set<V> neighbors, int targetTreewidth) {\n        if (neighbors.size() <= targetTreewidth) {\n            return 0.2; // Low potential if neighborhood already small\n        }\n\n        // Estimate how many edges would need to be added to make neighborhood a clique\n        AtomicInteger existingEdges = new AtomicInteger(0);\n        List<V> neighborList = new ArrayList<>(neighbors);\n\n        neighborList.parallelStream().forEach(n1 -> {\n            int index1 = neighborList.indexOf(n1);\n            long edgeCount = neighborList.stream()\n                    .skip(index1 + 1)\n                    .parallel()\n                    .filter(n2 -> graph.containsEdge(n1, n2) || graph.containsEdge(n2, n1))\n                    .count();\n            existingEdges.addAndGet((int) edgeCount);\n        });\n\n        int maxPossibleEdges = neighbors.size() * (neighbors.size() - 1) / 2;\n        int missingEdges = maxPossibleEdges - existingEdges.get();\n\n        // Higher missing edges = higher potential for treewidth increase if not removed\n        double missingRatio = maxPossibleEdges > 0 ? (double) missingEdges / maxPossibleEdges : 0.0;\n\n        // Scale by size relative to target treewidth\n        double sizeFactor = Math.min(2.0, (double) neighbors.size() / Math.max(1, targetTreewidth));\n\n        return missingRatio * sizeFactor;\n    }\n\n    /**\n     * Computes substructure potential impact.\n     */\n    private double computeSubstructurePotential(\n            Graph<V, DefaultEdge> graph, V vertex, Set<V> neighbors, int targetTreewidth) {\n        // Simple heuristic: vertices with many high-degree neighbors\n\n        return neighbors.parallelStream()\n                        .mapToInt(neighbor -> graph.inDegreeOf(neighbor) + graph.outDegreeOf(neighbor))\n                        .filter(degree -> degree > targetTreewidth)\n                        .count()\n                / (double) Math.max(1, neighbors.size());\n    }\n\n    /**\n     * Computes penalty for removing vertices that are crucial for connectivity.\n     */\n    private double computeConnectivityPreservationPenalty(Graph<V, DefaultEdge> graph, V vertex, int targetTreewidth) {\n        Set<V> neighbors = getNeighbors(vertex, graph);\n\n        // Penalty for removing vertices that maintain important connections\n        double penalty = 0.0;\n\n        // Factor 1: Bridge vertices get higher penalty\n        if (isBridgeVertex(graph, vertex, neighbors)) {\n            penalty += 0.3;\n        }\n\n        // Factor 2: Articulation points get penalty\n        if (isLikelyArticulationPoint(graph, vertex, neighbors)) {\n            penalty += 0.2;\n        }\n\n        // Factor 3: Vertices connecting different high-degree components\n        penalty += computeComponentConnectionPenalty(graph, vertex, neighbors, targetTreewidth);\n\n        return Math.min(0.5, penalty); // Cap penalty at 50% of score\n    }\n\n    /**\n     * Applies target treewidth specific adjustments to scores.\n     */\n    private void applyTargetTreewidthAdjustmentsParallel(\n            Graph<V, DefaultEdge> graph, Set<V> vertices, ConcurrentHashMap<V, Double> scores, int targetTreewidth) {\n\n        // Compute current graph statistics\n        DoubleSummaryStatistics scoreStats = scores.values().parallelStream()\n                .mapToDouble(Double::doubleValue)\n                .summaryStatistics();\n\n        // Apply adjustments in parallel\n        vertices.parallelStream().forEach(vertex -> {\n            double currentScore = scores.get(vertex);\n            double adjustedScore = currentScore;\n\n            // Adjustment 1: Boost vertices that significantly exceed target treewidth\n            int degree = graph.inDegreeOf(vertex) + graph.outDegreeOf(vertex);\n            if (degree > targetTreewidth * 1.5) {\n                adjustedScore *= 1.3; // 30% boost for high-degree vertices\n            }\n\n            // Adjustment 2: Normalize relative to target treewidth\n            double targetNormalizedFactor =\n                    1.0 + (double) Math.max(0, degree - targetTreewidth) / Math.max(1, targetTreewidth);\n            adjustedScore *= targetNormalizedFactor;\n\n            // Adjustment 3: Apply final bounds\n            adjustedScore = Math.max(0.0, Math.min(10.0, adjustedScore));\n\n            scores.put(vertex, adjustedScore);\n        });\n    }\n\n    /**\n     * Helper method to get all neighbors of a vertex.\n     */\n    private Set<V> getNeighbors(V vertex, Graph<V, DefaultEdge> graph) {\n        Set<V> neighbors = ConcurrentHashMap.newKeySet();\n\n        // Add in-neighbors\n        graph.incomingEdgesOf(vertex).parallelStream()\n                .map(graph::getEdgeSource)\n                .filter(neighbor -> !neighbor.equals(vertex))\n                .forEach(neighbors::add);\n\n        // Add out-neighbors\n        graph.outgoingEdgesOf(vertex).parallelStream()\n                .map(graph::getEdgeTarget)\n                .filter(neighbor -> !neighbor.equals(vertex))\n                .forEach(neighbors::add);\n\n        return neighbors;\n    }\n\n    /**\n     * Simple bridge vertex detection heuristic.\n     */\n    private boolean isBridgeVertex(Graph<V, DefaultEdge> graph, V vertex, Set<V> neighbors) {\n        if (neighbors.size() < 2) {\n            return false;\n        }\n\n        // Check if removal would significantly disconnect the neighborhood\n        long interNeighborConnections = neighbors.parallelStream()\n                        .mapToLong(n1 -> neighbors.parallelStream()\n                                .filter(n2 -> !n1.equals(n2))\n                                .filter(n2 -> graph.containsEdge(n1, n2) || graph.containsEdge(n2, n1))\n                                .count())\n                        .sum()\n                / 2; // Divide by 2 to avoid double counting\n\n        double expectedConnections = neighbors.size() * (neighbors.size() - 1) / 2.0;\n        return interNeighborConnections < expectedConnections * 0.3; // Less than 30% connected\n    }\n\n    /**\n     * Simple articulation point detection heuristic.\n     */\n    private boolean isLikelyArticulationPoint(Graph<V, DefaultEdge> graph, V vertex, Set<V> neighbors) {\n        return neighbors.size() >= 3 && isBridgeVertex(graph, vertex, neighbors);\n    }\n\n    /**\n     * Computes penalty for removing vertices that connect different components.\n     */\n    private double computeComponentConnectionPenalty(\n            Graph<V, DefaultEdge> graph, V vertex, Set<V> neighbors, int targetTreewidth) {\n        if (neighbors.size() < 2) {\n            return 0.0;\n        }\n\n        // Count high-degree neighbors (potential component representatives)\n        long highDegreeNeighbors = neighbors.parallelStream()\n                .mapToInt(neighbor -> graph.inDegreeOf(neighbor) + graph.outDegreeOf(neighbor))\n                .filter(degree -> degree > targetTreewidth)\n                .count();\n\n        if (highDegreeNeighbors >= 2) {\n            // Vertex connects multiple high-degree components\n            return Math.min(0.3, highDegreeNeighbors * 0.1);\n        }\n\n        return 0.0;\n    }\n\n    /**\n     * Utility method to safely shutdown thread pool.\n     */\n    private void shutdownThreadPool(ForkJoinPool threadPool) {\n        threadPool.shutdown();\n        try {\n            if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {\n                threadPool.shutdownNow();\n            }\n        } catch (InterruptedException e) {\n            threadPool.shutdownNow();\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    /**\n     * Alternative method for adaptive scoring based on current vs target treewidth.\n     * TODO: Revisit?\n     */\n    public ConcurrentHashMap<V, Double> computeAdaptiveVertexRemovalScore(\n            Graph<V, DefaultEdge> graph, int targetTreewidth, int currentTreewidth) {\n        ConcurrentHashMap<V, Double> baseScores = computeVertexRemovalScore(graph, targetTreewidth);\n\n        if (currentTreewidth <= targetTreewidth) {\n            return baseScores; // Already at or below target\n        }\n\n        // Apply adaptive scaling based on the gap between current and target treewidth\n        double scalingFactor = (double) (currentTreewidth - targetTreewidth) / Math.max(1, targetTreewidth);\n\n        baseScores.entrySet().parallelStream().forEach(entry -> {\n            double adjustedScore = entry.getValue() * (1.0 + scalingFactor);\n            entry.setValue(Math.min(10.0, adjustedScore));\n        });\n\n        return baseScores;\n    }\n\n    /**\n     * Computes structural importance of a vertex\n     */\n    private double computeStructuralImportance(Graph<V, DefaultEdge> graph, V vertex, double centrality) {\n        int degree = graph.degreeOf(vertex);\n        Set<V> neighbors = Graphs.neighborSetOf(graph, vertex);\n\n        // Count triangles involving this vertex\n        long triangles = neighbors.parallelStream()\n                        .mapToLong(n1 -> neighbors.stream()\n                                .filter(n2 -> !n1.equals(n2) && graph.containsEdge(n1, n2))\n                                .count())\n                        .sum()\n                / 2;\n\n        return degree + centrality * 10 + triangles * 0.5;\n    }\n\n    /**\n     * Computes betweenness centrality for all vertices\n     */\n    private Map<V, Double> originalComputeBetweennessCentrality(Graph<V, DefaultEdge> graph) {\n        Map<V, Double> centrality = new ConcurrentHashMap<>();\n        List<V> vertices = new ArrayList<>(graph.vertexSet());\n\n        // Initialize all centralities to 0\n        vertices.parallelStream().forEach(v -> centrality.put(v, 0.0));\n\n        // For efficiency, sample pairs of vertices for large graphs\n        // sampleSize and random were not used...\n        int sampleSize = Math.min(vertices.size() * (vertices.size() - 1) / 2, 1000);\n        Random random = new Random(42); // Fixed seed for reproducibility\n\n        vertices.parallelStream().limit(Math.min(50, vertices.size())).forEach(source -> {\n            Map<V, List<V>> predecessors = new HashMap<>();\n            Map<V, Integer> distances = new HashMap<>();\n            Map<V, Integer> pathCounts = new HashMap<>();\n            Stack<V> stack = new Stack<>();\n\n            // BFS from source\n            Queue<V> queue = new ArrayDeque<>();\n            queue.offer(source);\n            distances.put(source, 0);\n            pathCounts.put(source, 1);\n\n            while (!queue.isEmpty()) {\n                V current = queue.poll();\n                stack.push(current);\n\n                for (V neighbor : Graphs.neighborListOf(graph, current)) {\n                    if (!distances.containsKey(neighbor)) {\n                        distances.put(neighbor, distances.get(current) + 1);\n                        pathCounts.put(neighbor, 0);\n                        queue.offer(neighbor);\n                    }\n\n                    if (distances.get(neighbor) == distances.get(current) + 1) {\n                        pathCounts.put(neighbor, pathCounts.get(neighbor) + pathCounts.get(current));\n                        predecessors\n                                .computeIfAbsent(neighbor, k -> new ArrayList<>())\n                                .add(current);\n                    }\n                }\n            }\n\n            // Accumulate centrality values\n            Map<V, Double> dependency = new HashMap<>();\n            vertices.forEach(v -> dependency.put(v, 0.0));\n\n            while (!stack.isEmpty()) {\n                V vertex = stack.pop();\n                if (predecessors.containsKey(vertex)) {\n                    for (V predecessor : predecessors.get(vertex)) {\n                        double contribution = (pathCounts.get(predecessor) / (double) pathCounts.get(vertex))\n                                * (1.0 + dependency.get(vertex));\n                        dependency.put(predecessor, dependency.get(predecessor) + contribution);\n                    }\n                }\n\n                if (!vertex.equals(source)) {\n                    synchronized (centrality) {\n                        centrality.put(vertex, centrality.get(vertex) + dependency.get(vertex));\n                    }\n                }\n            }\n        });\n\n        return centrality;\n    }\n\n    /**\n     * Computes approximated betweenness centrality using random sampling.\n     *\n     * This implementation is based on Brandes' approximation algorithm that uses\n     * random sampling of source vertices to approximate betweenness centrality values.\n     * Instead of computing shortest paths from all vertices, we sample only a subset\n     * to achieve significant speedup while maintaining reasonable accuracy.\n     *\n     * @return a map containing approximate betweenness centrality values for each vertex\n     */\n    public Map<V, Double> computeBetweennessCentrality(Graph<V, DefaultEdge> graph) {\n        Set<V> vertices = graph.vertexSet();\n        int n = vertices.size();\n\n        if (n <= 2) {\n            // For very small graphs, return exact computation\n            return computeExactBetweennessCentrality(graph);\n        }\n\n        // Calculate sample size based on graph characteristics and desired accuracy\n        // Using the formula from Riondato & Kornaropoulos and Brandes & Pich research\n        double epsilon = 0.1; // Desired approximation error (can be made configurable)\n        double delta = 0.1; // Probability of exceeding error bound (can be made configurable)\n\n        // Compute sample size - various strategies exist in literature:\n        // 1. Fixed percentage of nodes (simple but effective)\n        // 2. Based on graph diameter and error bounds (more theoretical)\n        // 3. Adaptive sampling based on convergence\n\n        int sampleSize = Math.min(n, Math.max(10, (int) Math.ceil(\n                Math.log(2.0 / delta) / (2 * epsilon * epsilon) * Math.log(n) // Additional factor based on network size\n                )));\n\n        // For very large graphs, cap the sample size to ensure efficiency\n        if (n > 10000) {\n            sampleSize = Math.min(sampleSize, n / 10); // At most 10% of vertices\n        }\n\n        System.out.println(\"Computing approximated betweenness centrality with \" + sampleSize + \" samples out of \" + n\n                + \" vertices\");\n\n        // Initialize betweenness centrality scores\n        Map<V, Double> betweenness = new HashMap<>();\n        vertices.forEach(v -> betweenness.put(v, 0.0));\n\n        // Random number generator for sampling\n        Random random = ThreadLocalRandom.current();\n\n        // Convert vertices to list for random sampling\n        List<V> vertexList = new ArrayList<>(vertices);\n\n        // Sample source vertices and compute contributions\n        Set<V> sampledSources = sampleSourceVertices(graph, vertexList, sampleSize, random);\n\n        // Compute betweenness contributions from sampled sources\n        for (V source : sampledSources) {\n            Map<V, Double> contributions = computeSingleSourceBetweennessContributions(graph, source);\n\n            // Add contributions to total betweenness (scaled by sampling factor)\n            double scalingFactor = (double) n / sampleSize;\n            for (Map.Entry<V, Double> entry : contributions.entrySet()) {\n                V vertex = entry.getKey();\n                double contribution = entry.getValue() * scalingFactor;\n                betweenness.merge(vertex, contribution, Double::sum);\n            }\n        }\n\n        return betweenness;\n    }\n\n    /**\n     * Samples source vertices using different strategies based on graph characteristics.\n     *\n     * @param vertexList list of all vertices\n     * @param sampleSize number of vertices to sample\n     * @param random random number generator\n     * @return set of sampled source vertices\n     */\n    private Set<V> sampleSourceVertices(\n            Graph<V, DefaultEdge> graph, List<V> vertexList, int sampleSize, Random random) {\n        Set<V> sampledSources = new HashSet<>();\n\n        // Strategy 1: Degree-weighted sampling (Brandes & Pich approach)\n        // Higher degree vertices are more likely to be selected as they lie on more paths\n        if (shouldUseDegreeWeightedSampling(graph)) {\n            sampledSources = degreeWeightedSampling(graph, vertexList, sampleSize, random);\n        }\n        // Strategy 2: Uniform random sampling (simpler, often effective)\n        else {\n            sampledSources = uniformRandomSampling(vertexList, sampleSize, random);\n        }\n\n        return sampledSources;\n    }\n\n    /**\n     * Determines whether to use degree-weighted sampling based on graph characteristics.\n     */\n    private boolean shouldUseDegreeWeightedSampling(Graph<V, DefaultEdge> graph) {\n        // Use degree-weighted sampling for larger, more complex networks\n        return graph.vertexSet().size() > 100;\n    }\n\n    /**\n     * Performs degree-weighted random sampling of source vertices.\n     * Vertices with higher degrees have higher probability of being selected.\n     */\n    private Set<V> degreeWeightedSampling(\n            Graph<V, DefaultEdge> graph, List<V> vertexList, int sampleSize, Random random) {\n        Set<V> sampledSources = new HashSet<>();\n\n        // Calculate degree weights\n        Map<V, Integer> degrees = new HashMap<>();\n        int totalDegree = 0;\n\n        for (V vertex : vertexList) {\n            int degree = graph.inDegreeOf(vertex) + graph.outDegreeOf(vertex);\n            degrees.put(vertex, degree);\n            totalDegree += degree;\n        }\n\n        // If all vertices have degree 0, fall back to uniform sampling\n        if (totalDegree == 0) {\n            return uniformRandomSampling(vertexList, sampleSize, random);\n        }\n\n        // Sample vertices with probability proportional to their degree\n        while (sampledSources.size() < sampleSize && sampledSources.size() < vertexList.size()) {\n            double randomValue = random.nextDouble() * totalDegree;\n            double cumulativeWeight = 0;\n\n            for (V vertex : vertexList) {\n                if (sampledSources.contains(vertex)) continue;\n\n                cumulativeWeight += degrees.get(vertex);\n                if (randomValue <= cumulativeWeight) {\n                    sampledSources.add(vertex);\n                    break;\n                }\n            }\n\n            // Prevent infinite loop in edge cases\n            if (sampledSources.size() == vertexList.size()) break;\n        }\n\n        return sampledSources;\n    }\n\n    /**\n     * Performs uniform random sampling of source vertices.\n     */\n    private Set<V> uniformRandomSampling(List<V> vertexList, int sampleSize, Random random) {\n        Set<V> sampledSources = new HashSet<>();\n\n        // Use reservoir sampling for efficiency\n        for (int i = 0; i < Math.min(sampleSize, vertexList.size()); i++) {\n            V vertex;\n            do {\n                vertex = vertexList.get(random.nextInt(vertexList.size()));\n            } while (sampledSources.contains(vertex));\n\n            sampledSources.add(vertex);\n        }\n\n        return sampledSources;\n    }\n\n    /**\n     * Computes betweenness centrality contributions from a single source vertex.\n     * This is the core Brandes algorithm for single-source shortest paths.\n     *\n     * @param graph\n     * @param source the source vertex\n     * @return map of betweenness contributions for each vertex\n     */\n    private Map<V, Double> computeSingleSourceBetweennessContributions(Graph<V, DefaultEdge> graph, V source) {\n        Map<V, Double> contributions = new HashMap<>();\n        Map<V, List<V>> predecessors = new HashMap<>();\n        Map<V, Double> sigma = new HashMap<>(); // Number of shortest paths\n        Map<V, Integer> distance = new HashMap<>();\n        Map<V, Double> delta = new HashMap<>(); // Dependency values\n\n        // Initialize\n        graph.vertexSet().forEach(v -> {\n            predecessors.put(v, new ArrayList<>());\n            sigma.put(v, 0.0);\n            distance.put(v, -1);\n            delta.put(v, 0.0);\n            contributions.put(v, 0.0);\n        });\n\n        sigma.put(source, 1.0);\n        distance.put(source, 0);\n\n        // BFS to find shortest paths and count them\n        Queue<V> queue = new LinkedList<>();\n        Stack<V> stack = new Stack<>();\n        queue.offer(source);\n\n        while (!queue.isEmpty()) {\n            V vertex = queue.poll();\n            stack.push(vertex);\n\n            // Examine outgoing edges\n            for (DefaultEdge edge : graph.outgoingEdgesOf(vertex)) {\n                V neighbor = graph.getEdgeTarget(edge);\n\n                // First time visiting neighbor\n                if (distance.get(neighbor) < 0) {\n                    queue.offer(neighbor);\n                    distance.put(neighbor, distance.get(vertex) + 1);\n                }\n\n                // Shortest path to neighbor via vertex\n                if (distance.get(neighbor).equals(distance.get(vertex) + 1)) {\n                    sigma.put(neighbor, sigma.get(neighbor) + sigma.get(vertex));\n                    predecessors.get(neighbor).add(vertex);\n                }\n            }\n        }\n\n        // Accumulation phase - compute dependencies\n        while (!stack.isEmpty()) {\n            V vertex = stack.pop();\n\n            for (V predecessor : predecessors.get(vertex)) {\n                double contribution = (sigma.get(predecessor) / sigma.get(vertex)) * (1 + delta.get(vertex));\n                delta.put(predecessor, delta.get(predecessor) + contribution);\n            }\n\n            if (!vertex.equals(source)) {\n                contributions.put(vertex, delta.get(vertex));\n            }\n        }\n\n        return contributions;\n    }\n\n    /**\n     * Computes exact betweenness centrality for small graphs or when high precision is needed.\n     *\n     * @return map of exact betweenness centrality values\n     */\n    private Map<V, Double> computeExactBetweennessCentrality(Graph<V, DefaultEdge> graph) {\n        Map<V, Double> betweenness = new HashMap<>();\n        Set<V> vertices = graph.vertexSet();\n\n        // Initialize all betweenness values to 0\n        vertices.forEach(v -> betweenness.put(v, 0.0));\n\n        // Compute contributions from each vertex as source\n        for (V source : vertices) {\n            Map<V, Double> contributions = computeSingleSourceBetweennessContributions(graph, source);\n\n            for (Map.Entry<V, Double> entry : contributions.entrySet()) {\n                V vertex = entry.getKey();\n                betweenness.merge(vertex, entry.getValue(), Double::sum);\n            }\n        }\n\n        return betweenness;\n    }\n\n    /**\n     * Alternative adaptive sampling approach that adjusts sample size based on convergence.\n     * This can provide better accuracy guarantees but is more computationally expensive.\n     */\n    public Map<V, Double> computeBetweennessCentralityAdaptive(Graph<V, DefaultEdge> graph) {\n        Set<V> vertices = graph.vertexSet();\n        int n = vertices.size();\n\n        Map<V, Double> betweenness = new HashMap<>();\n        vertices.forEach(v -> betweenness.put(v, 0.0));\n\n        List<V> vertexList = new ArrayList<>(vertices);\n        Random random = ThreadLocalRandom.current();\n\n        int minSamples = Math.max(10, n / 100);\n        int maxSamples = Math.min(n, n / 2);\n\n        Map<V, Double> previousBetweenness = new HashMap<>(betweenness);\n        double convergenceThreshold = 0.01; // 1% change threshold\n\n        for (int sampleCount = minSamples; sampleCount <= maxSamples; sampleCount += minSamples) {\n            // Sample additional vertices\n            Set<V> newSamples = uniformRandomSampling(vertexList, minSamples, random);\n\n            // Compute contributions from new samples\n            for (V source : newSamples) {\n                Map<V, Double> contributions = computeSingleSourceBetweennessContributions(graph, source);\n                double scalingFactor = (double) n / sampleCount;\n\n                for (Map.Entry<V, Double> entry : contributions.entrySet()) {\n                    V vertex = entry.getKey();\n                    double contribution = entry.getValue() * scalingFactor;\n                    betweenness.merge(vertex, contribution, Double::sum);\n                }\n            }\n\n            // Check for convergence\n            if (hasConverged(betweenness, previousBetweenness, convergenceThreshold)) {\n                System.out.println(\"Converged after \" + sampleCount + \" samples\");\n                break;\n            }\n\n            previousBetweenness = new HashMap<>(betweenness);\n        }\n\n        return betweenness;\n    }\n\n    /**\n     * Checks if betweenness centrality values have converged.\n     */\n    private boolean hasConverged(Map<V, Double> current, Map<V, Double> previous, double threshold) {\n        for (V vertex : current.keySet()) {\n            double currentValue = current.get(vertex);\n            double previousValue = previous.getOrDefault(vertex, 0.0);\n\n            if (previousValue > 0) {\n                double relativeChange = Math.abs(currentValue - previousValue) / previousValue;\n                if (relativeChange > threshold) {\n                    return false;\n                }\n            } else if (currentValue > threshold) {\n                return false; // Significant change from zero\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Finds articulation points in the graph\n     */\n    private Set<V> findArticulationPoints(Graph<V, DefaultEdge> graph) {\n        Set<V> articulationPoints = ConcurrentHashMap.newKeySet();\n\n        for (V vertex : graph.vertexSet()) {\n            // Check if removing this vertex increases number of connected components\n            Graph<V, DefaultEdge> testGraph = new DefaultUndirectedGraph<>(DefaultEdge.class);\n\n            // Copy graph without the test vertex\n            graph.vertexSet().stream().filter(v -> !v.equals(vertex)).forEach(testGraph::addVertex);\n\n            graph.edgeSet().forEach(edge -> {\n                V source = graph.getEdgeSource(edge);\n                V target = graph.getEdgeTarget(edge);\n                if (!source.equals(vertex) && !target.equals(vertex)) {\n                    testGraph.addEdge(source, target);\n                }\n            });\n\n            // Count connected components\n            ConnectivityInspector<V, DefaultEdge> originalInspector = new ConnectivityInspector<>(graph);\n            ConnectivityInspector<V, DefaultEdge> testInspector = new ConnectivityInspector<>(testGraph);\n\n            if (testInspector.connectedSets().size()\n                    > originalInspector.connectedSets().size()) {\n                articulationPoints.add(vertex);\n            }\n        }\n\n        return articulationPoints;\n    }\n\n    /**\n     * Computes approximated betweenness centrality using random sampling.\n     *\n     * This implementation is based on Brandes' approximation algorithm that uses\n     * random sampling of source vertices to approximate betweenness centrality values.\n     * Instead of computing shortest paths from all vertices, we sample only a subset\n     * to achieve significant speedup while maintaining reasonable accuracy.\n     *\n     * @return a map containing approximate betweenness centrality values for each vertex\n     */\n    private Map<V, Double> computeBetweennessCentralityParallel(Graph<V, DefaultEdge> graph) {\n        return betweennessCentralityCache.computeIfAbsent(graph, g -> {\n            Set<V> vertices = g.vertexSet();\n            int n = vertices.size();\n\n            if (n <= 2) {\n                // For very small graphs, return exact computation\n                return computeExactBetweennessCentralityParallel(g);\n            }\n\n            // Calculate sample size based on graph characteristics and desired accuracy\n            double epsilon = 0.1; // Desired approximation error\n            double delta = 0.1; // Probability of exceeding error bound\n\n            int initialSampleSize = Math.min(\n                    n, Math.max(10, (int) Math.ceil(Math.log(2.0 / delta) / (2 * epsilon * epsilon) * Math.log(n))));\n\n            int sampleSize;\n            // For very large graphs, cap the sample size\n            if (n > 10000) {\n                sampleSize = Math.min(initialSampleSize, n / 10);\n            } else {\n                sampleSize = initialSampleSize;\n            }\n\n            System.out.println(\"Computing approximated betweenness centrality with \" + sampleSize + \" samples out of \"\n                    + n + \" vertices (parallel)\");\n\n            // Initialize concurrent betweenness centrality scores\n            ConcurrentHashMap<V, Double> betweenness = new ConcurrentHashMap<>();\n            vertices.parallelStream().forEach(v -> betweenness.put(v, 0.0));\n\n            // Thread-safe random number generator\n            ThreadLocalRandom random = ThreadLocalRandom.current();\n\n            // Convert vertices to concurrent list for thread-safe access\n            List<V> vertexList = new CopyOnWriteArrayList<>(vertices);\n\n            // Custom ForkJoinPool for better control over parallelization\n            ForkJoinPool customThreadPool = new ForkJoinPool(\n                    Math.min(\n                            Runtime.getRuntime().availableProcessors(),\n                            Math.max(1, sampleSize / 10)) // Scale threads based on sample size\n                    );\n\n            try {\n                CompletableFuture<Void> computation = CompletableFuture.runAsync(\n                        () -> {\n                            // Sample source vertices in parallel\n                            Set<V> sampledSources = sampleSourceVerticesParallel(g, vertexList, sampleSize, random);\n\n                            // Scaling factor for approximation\n                            double scalingFactor = (double) n / sampleSize;\n\n                            // Process sampled sources in parallel and accumulate results\n                            sampledSources.parallelStream().forEach(source -> {\n                                ConcurrentHashMap<V, Double> contributions =\n                                        computeSingleSourceBetweennessContributionsParallel(g, source);\n\n                                // Atomically update betweenness values with scaling\n                                contributions.entrySet().parallelStream().forEach(entry -> {\n                                    V vertex = entry.getKey();\n                                    double scaledContribution = entry.getValue() * scalingFactor;\n                                    betweenness.merge(vertex, scaledContribution, Double::sum);\n                                });\n                            });\n                        },\n                        customThreadPool);\n\n                // Wait for completion\n                computation.get();\n\n            } catch (InterruptedException | ExecutionException e) {\n                Thread.currentThread().interrupt();\n                throw new RuntimeException(\"Parallel betweenness centrality computation failed\", e);\n            } finally {\n                customThreadPool.shutdown();\n                try {\n                    if (!customThreadPool.awaitTermination(60, TimeUnit.SECONDS)) {\n                        customThreadPool.shutdownNow();\n                    }\n                } catch (InterruptedException e) {\n                    customThreadPool.shutdownNow();\n                    Thread.currentThread().interrupt();\n                }\n            }\n\n            return betweenness;\n        });\n    }\n\n    /**\n     * Samples source vertices using parallel processing with different sampling strategies.\n     */\n    private Set<V> sampleSourceVerticesParallel(\n            Graph<V, DefaultEdge> graph, List<V> vertexList, int sampleSize, ThreadLocalRandom random) {\n\n        if (shouldUseDegreeWeightedSampling(graph)) {\n            return degreeWeightedSamplingParallel(graph, vertexList, sampleSize, random);\n        } else {\n            return uniformRandomSamplingParallel(vertexList, sampleSize, random);\n        }\n    }\n\n    /**\n     * Performs degree-weighted random sampling using parallel streams.\n     */\n    private Set<V> degreeWeightedSamplingParallel(\n            Graph<V, DefaultEdge> graph, List<V> vertexList, int sampleSize, ThreadLocalRandom random) {\n\n        // Calculate degrees in parallel\n        ConcurrentMap<V, Integer> degrees = vertexList.parallelStream()\n                .collect(Collectors.toConcurrentMap(\n                        vertex -> vertex, vertex -> graph.inDegreeOf(vertex) + graph.outDegreeOf(vertex)));\n\n        // Calculate total degree\n        int totalDegree =\n                degrees.values().parallelStream().mapToInt(Integer::intValue).sum();\n\n        if (totalDegree == 0) {\n            return uniformRandomSamplingParallel(vertexList, sampleSize, random);\n        }\n\n        // Use concurrent set for thread-safe sampling\n        Set<V> sampledSources = ConcurrentHashMap.newKeySet();\n        AtomicInteger samplesNeeded = new AtomicInteger(sampleSize);\n\n        // Parallel sampling with retry mechanism\n        vertexList.parallelStream().filter(vertex -> samplesNeeded.get() > 0).forEach(vertex -> {\n            if (samplesNeeded.get() <= 0 || sampledSources.contains(vertex)) {\n                return;\n            }\n\n            // Thread-local random for each thread\n            ThreadLocalRandom localRandom = ThreadLocalRandom.current();\n            double probability = (double) degrees.get(vertex) / totalDegree;\n\n            // Adaptive probability to ensure we get enough samples\n            double adjustedProbability = Math.min(1.0, probability * sampleSize * 2.0 / vertexList.size());\n\n            if (localRandom.nextDouble() < adjustedProbability && sampledSources.size() < sampleSize) {\n\n                sampledSources.add(vertex);\n                samplesNeeded.decrementAndGet();\n            }\n        });\n\n        // Fill remaining slots with uniform sampling if needed\n        if (sampledSources.size() < sampleSize) {\n            Set<V> additionalSamples = vertexList.parallelStream()\n                    .filter(vertex -> !sampledSources.contains(vertex))\n                    .limit(sampleSize - sampledSources.size())\n                    .collect(Collectors.toSet());\n            sampledSources.addAll(additionalSamples);\n        }\n\n        return sampledSources;\n    }\n\n    /**\n     * Performs uniform random sampling using parallel streams.\n     */\n    private Set<V> uniformRandomSamplingParallel(List<V> vertexList, int sampleSize, ThreadLocalRandom random) {\n\n        // Use parallel stream to shuffle and take first sampleSize elements\n        return vertexList.parallelStream()\n                .unordered() // Allow parallel processing without ordering constraints\n                .distinct() // Ensure uniqueness\n                .limit(sampleSize)\n                .collect(Collectors.toConcurrentMap(\n                        vertex -> vertex, vertex -> ThreadLocalRandom.current().nextDouble()))\n                .entrySet()\n                .parallelStream()\n                .sorted(Map.Entry.comparingByValue()) // Sort by random values\n                .limit(sampleSize)\n                .map(Map.Entry::getKey)\n                .collect(Collectors.toSet());\n    }\n\n    /**\n     * Computes single-source betweenness contributions using parallel processing.\n     */\n    private ConcurrentHashMap<V, Double> computeSingleSourceBetweennessContributionsParallel(\n            Graph<V, DefaultEdge> graph, V source) {\n\n        Set<V> vertices = graph.vertexSet();\n        ConcurrentHashMap<V, Double> contributions = new ConcurrentHashMap<>();\n        ConcurrentHashMap<V, List<V>> predecessors = new ConcurrentHashMap<>();\n        ConcurrentHashMap<V, AtomicDouble> sigma = new ConcurrentHashMap<>();\n        ConcurrentHashMap<V, AtomicInteger> distance = new ConcurrentHashMap<>();\n        ConcurrentHashMap<V, AtomicDouble> delta = new ConcurrentHashMap<>();\n\n        // Parallel initialization\n        vertices.parallelStream().forEach(v -> {\n            predecessors.put(v, new CopyOnWriteArrayList<>());\n            sigma.put(v, new AtomicDouble(0.0));\n            distance.put(v, new AtomicInteger(-1));\n            delta.put(v, new AtomicDouble(0.0));\n            contributions.put(v, 0.0);\n        });\n\n        sigma.get(source).set(1.0);\n        distance.get(source).set(0);\n\n        // BFS with level-wise parallel processing\n        ConcurrentLinkedQueue<V> currentLevel = new ConcurrentLinkedQueue<>();\n        ConcurrentLinkedQueue<V> nextLevel = new ConcurrentLinkedQueue<>();\n        ConcurrentLinkedQueue<V> visitOrder = new ConcurrentLinkedQueue<>();\n\n        currentLevel.offer(source);\n\n        while (!currentLevel.isEmpty()) {\n            nextLevel.clear();\n\n            // Process current level\n            for (V vertex : currentLevel) {\n                visitOrder.offer(vertex);\n\n                // Examine outgoing edges\n                for (DefaultEdge edge : graph.outgoingEdgesOf(vertex)) {\n                    V neighbor = graph.getEdgeTarget(edge);\n                    int currentDist = distance.get(vertex).get();\n\n                    // Atomic check and update for first visit\n                    if (distance.get(neighbor).compareAndSet(-1, currentDist + 1)) {\n                        nextLevel.offer(neighbor);\n                    }\n\n                    // Check if this is a shortest path\n                    if (distance.get(neighbor).get() == currentDist + 1) {\n                        sigma.get(neighbor).addAndGet(sigma.get(vertex).get());\n                        predecessors.get(neighbor).add(vertex);\n                    }\n                }\n            }\n\n            // Swap levels\n            ConcurrentLinkedQueue<V> temp = currentLevel;\n            currentLevel = nextLevel;\n            nextLevel = temp;\n        }\n\n        // Accumulation phase - process in reverse order\n        List<V> reversedOrder = new ArrayList<>(visitOrder);\n        Collections.reverse(reversedOrder);\n\n        // Process accumulation in parallel batches to maintain dependencies\n        reversedOrder.parallelStream().forEach(vertex -> {\n            if (!vertex.equals(source)) {\n                // Process predecessors in parallel\n                predecessors.get(vertex).parallelStream().forEach(predecessor -> {\n                    double sigmaRatio =\n                            sigma.get(predecessor).get() / sigma.get(vertex).get();\n                    double contribution = sigmaRatio * (1 + delta.get(vertex).get());\n                    delta.get(predecessor).addAndGet(contribution);\n                });\n\n                contributions.put(vertex, delta.get(vertex).get());\n            }\n        });\n\n        return contributions;\n    }\n\n    /**\n     * Computes exact betweenness centrality for small graphs using parallel processing.\n     */\n    private ConcurrentHashMap<V, Double> computeExactBetweennessCentralityParallel(Graph<V, DefaultEdge> graph) {\n        Set<V> vertices = graph.vertexSet();\n        ConcurrentHashMap<V, Double> betweenness = new ConcurrentHashMap<>();\n\n        // Initialize in parallel\n        vertices.parallelStream().forEach(v -> betweenness.put(v, 0.0));\n\n        // Compute contributions from each vertex as source in parallel\n        vertices.parallelStream().forEach(source -> {\n            ConcurrentHashMap<V, Double> contributions =\n                    computeSingleSourceBetweennessContributionsParallel(graph, source);\n\n            // Atomically merge contributions\n            contributions.entrySet().parallelStream().forEach(entry -> {\n                betweenness.merge(entry.getKey(), entry.getValue(), Double::sum);\n            });\n        });\n\n        return betweenness;\n    }\n\n    /**\n     * Adaptive parallel sampling with convergence detection.\n     */\n    public ConcurrentHashMap<V, Double> computeBetweennessCentralityAdaptiveParallel(Graph<V, DefaultEdge> graph) {\n        Set<V> vertices = graph.vertexSet();\n        int n = vertices.size();\n\n        ConcurrentHashMap<V, Double> betweenness = new ConcurrentHashMap<>();\n        vertices.parallelStream().forEach(v -> betweenness.put(v, 0.0));\n\n        List<V> vertexList = new CopyOnWriteArrayList<>(vertices);\n        AtomicInteger totalSamples = new AtomicInteger(0);\n\n        int minSamples = Math.max(10, n / 100);\n        int maxSamples = Math.min(n, n / 2);\n        int batchSize = Math.max(1, minSamples / 4);\n\n        ConcurrentHashMap<V, Double> previousBetweenness = new ConcurrentHashMap<>(betweenness);\n        double convergenceThreshold = 0.01;\n\n        // Parallel adaptive sampling with convergence checking\n        IntStream.range(0, (maxSamples - minSamples) / batchSize + 1)\n                .parallel()\n                .takeWhile(batchIndex -> {\n                    int currentBatchStart = minSamples + batchIndex * batchSize;\n                    int currentBatchSize = Math.min(batchSize, maxSamples - currentBatchStart);\n\n                    if (currentBatchSize <= 0) return false;\n\n                    // Sample new batch in parallel\n                    Set<V> newSamples =\n                            uniformRandomSamplingParallel(vertexList, currentBatchSize, ThreadLocalRandom.current());\n\n                    // Compute contributions from new samples in parallel\n                    AtomicInteger currentTotal = new AtomicInteger(totalSamples.addAndGet(currentBatchSize));\n\n                    newSamples.parallelStream().forEach(source -> {\n                        ConcurrentHashMap<V, Double> contributions =\n                                computeSingleSourceBetweennessContributionsParallel(graph, source);\n\n                        double scalingFactor = (double) n / currentTotal.get();\n\n                        contributions.entrySet().parallelStream().forEach(entry -> {\n                            V vertex = entry.getKey();\n                            double contribution = entry.getValue() * scalingFactor;\n                            betweenness.merge(vertex, contribution, Double::sum);\n                        });\n                    });\n\n                    // Check convergence in parallel\n                    boolean converged = hasConvergedParallel(betweenness, previousBetweenness, convergenceThreshold);\n\n                    if (converged) {\n                        System.out.println(\"Converged after \" + currentTotal.get() + \" samples (parallel)\");\n                        return false; // Stop sampling\n                    }\n\n                    // Update previous values for next iteration\n                    previousBetweenness.clear();\n                    betweenness.entrySet().parallelStream()\n                            .forEach(entry -> previousBetweenness.put(entry.getKey(), entry.getValue()));\n\n                    return true; // Continue sampling\n                })\n                .forEach(batchIndex -> {\n                    /* Processing handled in takeWhile */\n                });\n\n        return betweenness;\n    }\n\n    /**\n     * Parallel convergence checking.\n     */\n    private boolean hasConvergedParallel(\n            ConcurrentHashMap<V, Double> current, ConcurrentHashMap<V, Double> previous, double threshold) {\n\n        return current.entrySet().parallelStream().allMatch(entry -> {\n            V vertex = entry.getKey();\n            double currentValue = entry.getValue();\n            double previousValue = previous.getOrDefault(vertex, 0.0);\n\n            if (previousValue > 0) {\n                double relativeChange = Math.abs(currentValue - previousValue) / previousValue;\n                return relativeChange <= threshold;\n            } else {\n                return currentValue <= threshold;\n            }\n        });\n    }\n\n    /**\n     * Utility method to get thread-safe metrics about the sampling process.\n     */\n    public ConcurrentHashMap<String, Double> getSamplingMetrics(int sampleSize, int totalVertices) {\n        ConcurrentHashMap<String, Double> metrics = new ConcurrentHashMap<>();\n\n        metrics.put(\"sample_ratio\", (double) sampleSize / totalVertices);\n        metrics.put(\"expected_speedup\", (double) totalVertices / sampleSize);\n        metrics.put(\n                \"parallel_efficiency\",\n                (double) Runtime.getRuntime().availableProcessors() / Math.max(1, sampleSize / 10));\n\n        return metrics;\n    }\n\n    /**\n     * Computes quality score for a modulator\n     */\n    private double computeModulatorQuality(Graph<V, E> graph, Set<V> modulator, int targetTreewidth) {\n        int resultingTreewidth = treewidthComputer.computeEta(graph, modulator);\n\n        if (resultingTreewidth > targetTreewidth) {\n            return Double.MAX_VALUE; // Invalid solution\n        }\n\n        // Quality = size penalty + treewidth penalty\n        return modulator.size() + (resultingTreewidth * 0.1);\n    }\n\n    /**\n     * Converts directed graph to undirected\n     */\n    private Graph<V, DefaultEdge> convertToUndirected(Graph<V, E> directed) {\n        Graph<V, DefaultEdge> undirected = new DefaultUndirectedGraph<>(DefaultEdge.class);\n\n        directed.vertexSet().forEach(undirected::addVertex);\n\n        directed.edgeSet().forEach(edge -> {\n            V source = directed.getEdgeSource(edge);\n            V target = directed.getEdgeTarget(edge);\n            if (!source.equals(target) && !undirected.containsEdge(source, target)) {\n                undirected.addEdge(source, target);\n            }\n        });\n\n        return undirected;\n    }\n\n    /**\n     * Fallback modulator computation\n     */\n    private ModulatorResult<V> computeFallbackModulator(Graph<V, E> graph, int targetTreewidth, int maxSize) {\n        Set<V> modulator = graph.vertexSet().stream()\n                .sorted((v1, v2) -> Integer.compare(\n                        graph.inDegreeOf(v2) + graph.outDegreeOf(v2), graph.inDegreeOf(v1) + graph.outDegreeOf(v1)))\n                .limit(maxSize)\n                .collect(Collectors.toSet());\n\n        return new ModulatorResult<>(\n                modulator,\n                treewidthComputer.computeEta(graph, modulator),\n                computeModulatorQuality(graph, modulator, targetTreewidth));\n    }\n\n    private Set<V> getFutureValue(Future<Set<V>> future) {\n        try {\n            return future.get();\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    public void shutdown() {\n        treewidthComputer.shutdown();\n        fvsComputer.shutdown();\n        if (executorService != null && !executorService.isShutdown()) {\n            executorService.shutdown();\n        }\n    }\n\n    /**\n     * Result container for modulator computation\n     */\n    public static class ModulatorResult<V> {\n        private final Set<V> modulator;\n        private final int resultingTreewidth;\n        private final double qualityScore;\n\n        public ModulatorResult(Set<V> modulator, int resultingTreewidth, double qualityScore) {\n            this.modulator = new HashSet<>(modulator);\n            this.resultingTreewidth = resultingTreewidth;\n            this.qualityScore = qualityScore;\n        }\n\n        public Set<V> getModulator() {\n            return new HashSet<>(modulator);\n        }\n\n        public int getResultingTreewidth() {\n            return resultingTreewidth;\n        }\n\n        public double getQualityScore() {\n            return qualityScore;\n        }\n\n        public int getSize() {\n            return modulator.size();\n        }\n\n        @Override\n        public String toString() {\n            return String.format(\n                    \"ModulatorResult{size=%d, treewidth=%d, quality=%.2f}\",\n                    modulator.size(), resultingTreewidth, qualityScore);\n        }\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/kernelized/ParameterComputer.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport java.util.HashSet;\nimport java.util.Set;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\n\n/**\n * Main facade for computing eta and k parameters needed for DirectedFeedbackVertexSetSolver\n * Generated by Perplexity.ai's Research model\n */\npublic class ParameterComputer<V, E> {\n\n    private final TreewidthComputer<V, E> treewidthComputer;\n    private final FeedbackVertexSetComputer<V, E> fvsComputer;\n\n    public ParameterComputer(SuperTypeToken<E> edgeTypeToken) {\n        this.treewidthComputer = new TreewidthComputer<>();\n        this.fvsComputer = new FeedbackVertexSetComputer<>(edgeTypeToken);\n    }\n\n    public ParameterComputer(SuperTypeToken<E> edgeTypeToken, int parallelismLevel) {\n        this.treewidthComputer = new TreewidthComputer<>(parallelismLevel);\n        this.fvsComputer = new FeedbackVertexSetComputer<>(edgeTypeToken, parallelismLevel);\n    }\n\n    /**\n     * Computes both eta and k parameters\n     */\n    public Parameters computeParameters(Graph<V, E> graph) {\n        return computeParameters(graph, new HashSet<>());\n    }\n\n    /**\n     * Computes eta and k with a given modulator\n     */\n    public Parameters computeParameters(Graph<V, E> graph, Set<V> modulator) {\n        int eta = treewidthComputer.computeEta(graph, modulator);\n        int k = fvsComputer.computeK(graph);\n\n        return new Parameters(k, modulator.size(), eta);\n    }\n\n    /**\n     * Computes a good modulator and then the parameters\n     */\n    public Parameters computeParametersWithOptimalModulator(Graph<V, E> graph, int maxModulatorSize) {\n        Set<V> bestModulator = findGoodModulator(graph, maxModulatorSize);\n        return computeParameters(graph, bestModulator);\n    }\n\n    /**\n     * Finds a good treewidth modulator using various heuristics\n     */\n    private Set<V> findGoodModulator(Graph<V, E> graph, int maxSize) {\n        if (maxSize <= 0) return new HashSet<>();\n\n        // Try different modulator finding strategies\n        Set<V> degreeBasedModulator = findDegreeBasedModulator(graph, maxSize);\n        Set<V> fvsBasedModulator = findFeedbackVertexSetBasedModulator(graph, maxSize);\n\n        // Choose the one that gives better treewidth\n        int etaDegree = treewidthComputer.computeEta(graph, degreeBasedModulator);\n        int etaFVS = treewidthComputer.computeEta(graph, fvsBasedModulator);\n\n        return etaDegree <= etaFVS ? degreeBasedModulator : fvsBasedModulator;\n    }\n\n    private Set<V> findDegreeBasedModulator(Graph<V, E> graph, int maxSize) {\n        return graph.vertexSet().parallelStream()\n                .sorted((v1, v2) -> Integer.compare(\n                        graph.inDegreeOf(v2) + graph.outDegreeOf(v2), graph.inDegreeOf(v1) + graph.outDegreeOf(v1)))\n                .limit(maxSize)\n                .collect(java.util.stream.Collectors.toSet());\n    }\n\n    private Set<V> findFeedbackVertexSetBasedModulator(Graph<V, E> graph, int maxSize) {\n        Set<V> fvs = fvsComputer.greedyFeedbackVertexSet(graph);\n        if (fvs.size() <= maxSize) {\n            return fvs;\n        } else {\n            return fvs.stream().limit(maxSize).collect(java.util.stream.Collectors.toSet());\n        }\n    }\n\n    public void shutdown() {\n        treewidthComputer.shutdown();\n        fvsComputer.shutdown();\n    }\n\n    /**\n     * Result container for computed parameters\n     */\n    public static class Parameters {\n        private final int k; // feedback vertex set size\n        private final int modulatorSize; // modulator size (ℓ)\n        private final int eta; // treewidth after modulator removal\n\n        public Parameters(int k, int modulatorSize, int eta) {\n            this.k = k;\n            this.modulatorSize = modulatorSize;\n            this.eta = eta;\n        }\n\n        public int getK() {\n            return k;\n        }\n\n        public int getModulatorSize() {\n            return modulatorSize;\n        }\n\n        public int getEta() {\n            return eta;\n        }\n\n        @Override\n        public String toString() {\n            return String.format(\"Parameters{k=%d, ℓ=%d, η=%d}\", k, modulatorSize, eta);\n        }\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/main/java/org/hjug/feedback/vertex/kernelized/TreewidthComputer.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\nimport org.jgrapht.Graph;\nimport org.jgrapht.Graphs;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.jgrapht.graph.DefaultUndirectedGraph;\n\n/**\n * Multithreaded treewidth computer that implements multiple heuristic algorithms\n * for approximating treewidth of graphs after modulator removal.\n * Generated by Perplexity.ai's Research model from papers by Hans L. Bodlaender et al.\n * https://dl.acm.org/doi/10.1137/S0097539793251219\n * https://dl.acm.org/doi/10.1145/2973749\n *\n */\npublic class TreewidthComputer<V, E> {\n\n    private final ExecutorService executorService;\n\n    public TreewidthComputer() {\n        this.executorService = ForkJoinPool.commonPool();\n    }\n\n    public TreewidthComputer(int parallelismLevel) {\n        this.executorService = Executors.newWorkStealingPool(parallelismLevel);\n    }\n\n    /**\n     * Computes eta (η): the treewidth of the undirected version of the graph\n     * after removing the modulator vertices.\n     */\n    public int computeEta(Graph<V, E> graph, Set<V> modulator) {\n        // Convert to undirected graph and remove modulator\n        Graph<V, DefaultEdge> undirectedGraph = convertToUndirectedWithoutModulator(graph, modulator);\n\n        // shortcuts\n        if (undirectedGraph.vertexSet().isEmpty() || undirectedGraph.vertexSet().size() == 1) {\n            return 0;\n        } else if (!hasCycles(graph)) {\n            // A graph without cycles will have an eta of 1 for our purposes\n            // since a graph that does not have cycles is not of interest\n            return 1;\n        }\n\n        // Run multiple treewidth approximation algorithms in parallel\n        List<Callable<Integer>> algorithms = Arrays.asList(\n                () -> minDegreeEliminationTreewidth(undirectedGraph),\n                () -> fillInHeuristicTreewidth(undirectedGraph),\n                () -> maxCliqueTreewidth(undirectedGraph),\n                () -> greedyTriangulationTreewidth(undirectedGraph));\n\n        try {\n            List<Future<Integer>> results = executorService.invokeAll(algorithms, 30, TimeUnit.SECONDS);\n\n            return results.parallelStream()\n                    .map(this::getFutureValue)\n                    .filter(Objects::nonNull)\n                    .filter(eta -> eta > 1) // if a graph has a cycle, eta will be more than 1\n                    .min(Integer::compareTo)\n                    .orElse(undirectedGraph.vertexSet().size() - 1); // Worst case bound\n\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            return computeFallbackTreewidth(undirectedGraph);\n        }\n    }\n\n    /**\n     * Checks if the graph has cycles\n     */\n    private boolean hasCycles(Graph<V, E> graph) {\n        CycleDetector<V, E> detector = new CycleDetector<>(graph);\n        return detector.detectCycles();\n    }\n\n    /**\n     * Converts directed/undirected graph to undirected and removes modulator vertices\n     */\n    private Graph<V, DefaultEdge> convertToUndirectedWithoutModulator(Graph<V, E> original, Set<V> modulator) {\n        Graph<V, DefaultEdge> undirected = new DefaultUndirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices (except modulator)\n        original.vertexSet().stream().filter(v -> !modulator.contains(v)).forEach(undirected::addVertex);\n\n        // Add edges\n        original.edgeSet().parallelStream().forEach(edge -> {\n            V source = original.getEdgeSource(edge);\n            V target = original.getEdgeTarget(edge);\n\n            if (undirected.containsVertex(source)\n                    && undirected.containsVertex(target)\n                    && !source.equals(target)\n                    && !undirected.containsEdge(source, target)) {\n\n                synchronized (undirected) {\n                    if (!undirected.containsEdge(source, target)) {\n                        undirected.addEdge(source, target);\n                    }\n                }\n            }\n        });\n\n        return undirected;\n    }\n\n    /**\n     * Minimum degree elimination ordering heuristic\n     */\n    private int minDegreeEliminationTreewidth(Graph<V, DefaultEdge> graph) {\n        Set<V> remainingVertices =\n                new ConcurrentHashMap<>(graph.vertexSet().stream().collect(Collectors.toMap(v -> v, v -> v))).keySet();\n\n        Map<V, Set<V>> adjacencyMap = new ConcurrentHashMap<>();\n\n        // Initialize adjacency map\n        graph.vertexSet().parallelStream().forEach(v -> {\n            adjacencyMap.put(v, ConcurrentHashMap.newKeySet());\n            adjacencyMap.get(v).addAll(Graphs.neighborSetOf(graph, v));\n        });\n\n        int maxBagSize = 0;\n\n        while (!remainingVertices.isEmpty()) {\n            // Find vertex with minimum degree\n            V minDegreeVertex = remainingVertices.parallelStream()\n                    .min(Comparator.comparingInt(v -> (int) adjacencyMap.get(v).stream()\n                            .filter(remainingVertices::contains)\n                            .count()))\n                    .orElse(null);\n\n            if (minDegreeVertex == null) break;\n\n            Set<V> neighbors = adjacencyMap.get(minDegreeVertex).stream()\n                    .filter(remainingVertices::contains)\n                    .collect(Collectors.toSet());\n\n            maxBagSize = Math.max(maxBagSize, neighbors.size());\n\n            // Make neighbors a clique\n            neighbors.parallelStream().forEach(u -> {\n                neighbors.parallelStream().filter(v -> !v.equals(u)).forEach(v -> {\n                    adjacencyMap.get(u).add(v);\n                    adjacencyMap.get(v).add(u);\n                });\n            });\n\n            remainingVertices.remove(minDegreeVertex);\n        }\n\n        return maxBagSize;\n    }\n\n    /**\n     * Computes an upper bound on treewidth using the minimum fill-in heuristic with parallelization.\n     *\n     * The minimum fill-in heuristic repeatedly eliminates the vertex that requires\n     * the minimum number of edges to be added to make its neighborhood a clique.\n     * This implementation uses parallel streams and concurrent data structures for better performance.\n     *\n     * @return an upper bound on the treewidth of the graph\n     */\n    public int fillInHeuristicTreewidth(Graph<V, DefaultEdge> graph) {\n        if (graph.vertexSet().isEmpty()) {\n            return 0;\n        }\n\n        // Create a working copy of the graph using concurrent data structures\n        ConcurrentHashMap<V, Set<V>> adjacencyMap = new ConcurrentHashMap<>();\n\n        // Initialize adjacency map in parallel\n        graph.vertexSet().parallelStream().forEach(vertex -> {\n            Set<V> neighbors = ConcurrentHashMap.newKeySet();\n\n            // Add in-neighbors\n            graph.incomingEdgesOf(vertex).parallelStream()\n                    .map(graph::getEdgeSource)\n                    .filter(neighbor -> !neighbor.equals(vertex))\n                    .forEach(neighbors::add);\n\n            // Add out-neighbors\n            graph.outgoingEdgesOf(vertex).parallelStream()\n                    .map(graph::getEdgeTarget)\n                    .filter(neighbor -> !neighbor.equals(vertex))\n                    .forEach(neighbors::add);\n\n            adjacencyMap.put(vertex, neighbors);\n        });\n\n        AtomicInteger maxCliqueSize = new AtomicInteger(0);\n        ConcurrentHashMap<V, Boolean> remainingVertices = new ConcurrentHashMap<>();\n\n        // Initialize remaining vertices\n        graph.vertexSet().parallelStream().forEach(vertex -> remainingVertices.put(vertex, true));\n\n        // Custom ForkJoinPool for better control over parallelization\n        ForkJoinPool customThreadPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());\n\n        try {\n            // Main elimination loop\n            while (!remainingVertices.isEmpty()) {\n\n                // Find vertex with minimum fill-in in parallel\n                Optional<Map.Entry<V, Integer>> bestVertexEntry = customThreadPool\n                        .submit(() -> remainingVertices.keySet().parallelStream()\n                                .collect(Collectors.toConcurrentMap(\n                                        vertex -> vertex,\n                                        vertex -> calculateFillInParallel(vertex, adjacencyMap, remainingVertices)))\n                                .entrySet()\n                                .parallelStream()\n                                .min(Map.Entry.comparingByValue()))\n                        .get();\n\n                if (!bestVertexEntry.isPresent()) {\n                    // Fallback: choose any remaining vertex\n                    V fallbackVertex = remainingVertices.keys().nextElement();\n                    eliminateVertexParallel(fallbackVertex, adjacencyMap, remainingVertices, maxCliqueSize);\n                } else {\n                    V bestVertex = bestVertexEntry.get().getKey();\n                    eliminateVertexParallel(bestVertex, adjacencyMap, remainingVertices, maxCliqueSize);\n                }\n            }\n        } catch (InterruptedException | ExecutionException e) {\n            Thread.currentThread().interrupt();\n            throw new RuntimeException(\"Parallel computation interrupted\", e);\n        } finally {\n            customThreadPool.shutdown();\n        }\n\n        return maxCliqueSize.get();\n    }\n\n    /**\n     * Alternative implementation using CompletableFuture for more complex parallel operations.\n     * TODO: Explore later\n     */\n    public CompletableFuture<Integer> fillInHeuristicTreewidthAsync(Graph<V, DefaultEdge> graph) {\n        return CompletableFuture.supplyAsync(() -> {\n            if (graph.vertexSet().isEmpty()) {\n                return 0;\n            }\n\n            // Initialize concurrent data structures\n            ConcurrentHashMap<V, Set<V>> adjacencyMap = new ConcurrentHashMap<>();\n            ConcurrentHashMap<V, Boolean> remainingVertices = new ConcurrentHashMap<>();\n            AtomicInteger maxCliqueSize = new AtomicInteger(0);\n\n            // Parallel initialization\n            List<CompletableFuture<Void>> initFutures = graph.vertexSet().stream()\n                    .map(vertex -> CompletableFuture.runAsync(() -> {\n                        Set<V> neighbors = ConcurrentHashMap.newKeySet();\n\n                        graph.incomingEdgesOf(vertex).parallelStream()\n                                .map(graph::getEdgeSource)\n                                .filter(neighbor -> !neighbor.equals(vertex))\n                                .forEach(neighbors::add);\n\n                        graph.outgoingEdgesOf(vertex).parallelStream()\n                                .map(graph::getEdgeTarget)\n                                .filter(neighbor -> !neighbor.equals(vertex))\n                                .forEach(neighbors::add);\n\n                        adjacencyMap.put(vertex, neighbors);\n                        remainingVertices.put(vertex, true);\n                    }))\n                    .collect(Collectors.toList());\n\n            // Wait for initialization to complete\n            CompletableFuture.allOf(initFutures.toArray(new CompletableFuture[0]))\n                    .join();\n\n            // Main elimination loop\n            while (!remainingVertices.isEmpty()) {\n                CompletableFuture<V> bestVertexFuture =\n                        CompletableFuture.supplyAsync(() -> remainingVertices.keySet().parallelStream()\n                                .min(Comparator.comparingInt(\n                                        vertex -> calculateFillInParallel(vertex, adjacencyMap, remainingVertices)))\n                                .orElse(remainingVertices.keys().nextElement()));\n\n                V bestVertex = bestVertexFuture.join();\n                eliminateVertexParallel(bestVertex, adjacencyMap, remainingVertices, maxCliqueSize);\n            }\n\n            return maxCliqueSize.get();\n        });\n    }\n\n    /**\n     * Eliminates a vertex and updates the graph structure in parallel.\n     *\n     * @param vertex the vertex to eliminate\n     * @param adjacencyMap the current adjacency representation\n     * @param remainingVertices vertices that haven't been eliminated yet\n     * @param maxCliqueSize atomic reference to track maximum clique size\n     */\n    private void eliminateVertexParallel(\n            V vertex,\n            ConcurrentHashMap<V, Set<V>> adjacencyMap,\n            ConcurrentHashMap<V, Boolean> remainingVertices,\n            AtomicInteger maxCliqueSize) {\n        Set<V> neighborhood = getNeighborhoodParallel(vertex, adjacencyMap, remainingVertices);\n\n        // Update maximum clique size atomically\n        maxCliqueSize.updateAndGet(current -> Math.max(current, neighborhood.size()));\n\n        // Make the neighborhood a clique in parallel\n        fillInNeighborhoodParallel(neighborhood, adjacencyMap);\n\n        // Remove the eliminated vertex\n        remainingVertices.remove(vertex);\n        adjacencyMap.remove(vertex);\n\n        // Remove vertex from all neighbor sets in parallel\n        adjacencyMap.values().parallelStream().forEach(neighbors -> neighbors.remove(vertex));\n    }\n\n    /**\n     * Gets the neighborhood of a vertex using parallel processing.\n     *\n     * @param vertex the vertex whose neighborhood to find\n     * @param adjacencyMap the current adjacency representation\n     * @param remainingVertices vertices that haven't been eliminated yet\n     * @return the set of neighboring vertices that are still remaining\n     */\n    private Set<V> getNeighborhoodParallel(\n            V vertex, ConcurrentHashMap<V, Set<V>> adjacencyMap, ConcurrentHashMap<V, Boolean> remainingVertices) {\n        Set<V> allNeighbors = adjacencyMap.getOrDefault(vertex, ConcurrentHashMap.newKeySet());\n\n        // Filter to only remaining vertices in parallel\n        return allNeighbors.parallelStream()\n                .filter(remainingVertices::containsKey)\n                .collect(Collectors.toConcurrentMap(\n                        neighbor -> neighbor,\n                        neighbor -> true,\n                        (existing, replacement) -> true,\n                        ConcurrentHashMap::new))\n                .keySet();\n    }\n\n    /**\n     * Adds edges to make the given set of vertices form a clique using parallel processing.\n     *\n     * @param vertices the vertices that should form a clique\n     * @param adjacencyMap the adjacency map to modify\n     */\n    private void fillInNeighborhoodParallel(Set<V> vertices, ConcurrentHashMap<V, Set<V>> adjacencyMap) {\n        List<V> vertexList = new ArrayList<>(vertices);\n\n        // Add all missing edges to make it a clique in parallel\n        vertexList.parallelStream().forEach(v1 -> {\n            int index1 = vertexList.indexOf(v1);\n            vertexList.stream().skip(index1 + 1).parallel().forEach(v2 -> {\n                // Add edges in both directions atomically\n                adjacencyMap\n                        .computeIfAbsent(v1, k -> ConcurrentHashMap.newKeySet())\n                        .add(v2);\n                adjacencyMap\n                        .computeIfAbsent(v2, k -> ConcurrentHashMap.newKeySet())\n                        .add(v1);\n            });\n        });\n    }\n\n    /**\n     * Calculates the fill-in value for a vertex using parallel processing.\n     *\n     * @param vertex the vertex to calculate fill-in for\n     * @param adjacencyMap the current adjacency representation\n     * @param remainingVertices vertices that haven't been eliminated yet\n     * @return the number of edges needed to make the neighborhood a clique\n     */\n    private int calculateFillInParallel(\n            V vertex, ConcurrentHashMap<V, Set<V>> adjacencyMap, ConcurrentHashMap<V, Boolean> remainingVertices) {\n        Set<V> neighborhood = getNeighborhoodParallel(vertex, adjacencyMap, remainingVertices);\n\n        if (neighborhood.size() <= 1) {\n            return 0; // Already a clique (or empty)\n        }\n\n        List<V> neighborList = new ArrayList<>(neighborhood);\n\n        // Count missing edges in parallel\n        return neighborList.parallelStream()\n                .mapToInt(v1 -> {\n                    int index1 = neighborList.indexOf(v1);\n                    return (int) neighborList.stream()\n                            .skip(index1 + 1)\n                            .parallel()\n                            .filter(v2 -> !hasEdgeParallel(v1, v2, adjacencyMap))\n                            .count();\n                })\n                .sum();\n    }\n\n    /**\n     * Checks if an edge exists between two vertices.\n     *\n     * @param v1 first vertex\n     * @param v2 second vertex\n     * @param adjacencyMap the current adjacency representation\n     * @return true if an edge exists in either direction\n     */\n    private boolean hasEdgeParallel(V v1, V v2, ConcurrentHashMap<V, Set<V>> adjacencyMap) {\n        Set<V> neighborsV1 = adjacencyMap.get(v1);\n        Set<V> neighborsV2 = adjacencyMap.get(v2);\n\n        return (neighborsV1 != null && neighborsV1.contains(v2)) || (neighborsV2 != null && neighborsV2.contains(v1));\n    }\n\n    /**\n     * Maximum clique based treewidth lower bound\n     */\n    private int maxCliqueTreewidth(Graph<V, DefaultEdge> graph) {\n        if (graph.vertexSet().size() <= 50) {\n            return findMaxCliqueBronKerbosch(graph) - 1;\n        } else {\n            return findMaxCliqueGreedy(graph) - 1;\n        }\n    }\n\n    /**\n     * Greedy triangulation heuristic\n     */\n    private int greedyTriangulationTreewidth(Graph<V, DefaultEdge> graph) {\n        Map<V, Set<V>> adjacencyMap = new ConcurrentHashMap<>();\n\n        // Initialize adjacency map\n        graph.vertexSet().parallelStream().forEach(v -> {\n            adjacencyMap.put(v, ConcurrentHashMap.newKeySet());\n            adjacencyMap.get(v).addAll(Graphs.neighborSetOf(graph, v));\n        });\n\n        int maxBagSize = 0;\n        Queue<V> eliminationOrder = new ConcurrentLinkedQueue<>(graph.vertexSet());\n\n        while (!eliminationOrder.isEmpty()) {\n            V vertex = eliminationOrder.poll();\n            if (vertex == null) break;\n\n            Set<V> neighbors = adjacencyMap.get(vertex);\n            maxBagSize = Math.max(maxBagSize, neighbors.size());\n\n            // Triangulate neighborhood\n            triangulateNeighborhood(neighbors, adjacencyMap);\n        }\n\n        return maxBagSize;\n    }\n\n    private void triangulateNeighborhood(Set<V> neighbors, Map<V, Set<V>> adjacencyMap) {\n        List<V> neighborList = new ArrayList<>(neighbors);\n        neighborList.parallelStream().forEach(u -> {\n            neighborList.parallelStream()\n                    .filter(v -> !v.equals(u) && !adjacencyMap.get(u).contains(v))\n                    .forEach(v -> {\n                        adjacencyMap.get(u).add(v);\n                        adjacencyMap.get(v).add(u);\n                    });\n        });\n    }\n\n    // original implementation\n    private int calculateFillIn(Set<V> neighbors, Map<V, Set<V>> adjacencyMap) {\n        AtomicInteger fillIn = new AtomicInteger(0);\n\n        neighbors.parallelStream().forEach(u -> {\n            neighbors.parallelStream()\n                    .filter(v -> !v.equals(u) && !adjacencyMap.get(u).contains(v))\n                    .forEach(v -> fillIn.incrementAndGet());\n        });\n\n        return fillIn.get() / 2; // Each edge counted twice\n    }\n\n    private int findMaxCliqueBronKerbosch(Graph<V, DefaultEdge> graph) {\n        Set<V> R = new HashSet<>();\n        Set<V> P = new HashSet<>(graph.vertexSet());\n        Set<V> X = new HashSet<>();\n        AtomicInteger maxCliqueSize = new AtomicInteger(0);\n\n        bronKerbosch(graph, R, P, X, maxCliqueSize);\n        return maxCliqueSize.get();\n    }\n\n    private void bronKerbosch(Graph<V, DefaultEdge> graph, Set<V> R, Set<V> P, Set<V> X, AtomicInteger maxSize) {\n        if (P.isEmpty() && X.isEmpty()) {\n            maxSize.set(Math.max(maxSize.get(), R.size()));\n            return;\n        }\n\n        for (V vertex : new HashSet<>(P)) {\n            Set<V> neighbors = Graphs.neighborSetOf(graph, vertex);\n\n            Set<V> newR = new HashSet<>(R);\n            newR.add(vertex);\n\n            Set<V> newP = new HashSet<>(P);\n            newP.retainAll(neighbors);\n\n            Set<V> newX = new HashSet<>(X);\n            newX.retainAll(neighbors);\n\n            bronKerbosch(graph, newR, newP, newX, maxSize);\n\n            P.remove(vertex);\n            X.add(vertex);\n        }\n    }\n\n    private int findMaxCliqueGreedy(Graph<V, DefaultEdge> graph) {\n        return graph.vertexSet().parallelStream()\n                .mapToInt(v -> Graphs.neighborSetOf(graph, v).size() + 1)\n                .max()\n                .orElse(1);\n    }\n\n    private int computeFallbackTreewidth(Graph<V, DefaultEdge> graph) {\n        // Simple fallback: maximum degree\n        return graph.vertexSet().parallelStream()\n                .mapToInt(v -> graph.degreeOf(v))\n                .max()\n                .orElse(0);\n    }\n\n    private Integer getFutureValue(Future<Integer> future) {\n        try {\n            return future.get();\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    public void shutdown() {\n        if (executorService != null && !executorService.isShutdown()) {\n            executorService.shutdown();\n        }\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/dsm/CircularReferenceCheckerTests.java",
    "content": "package org.hjug.dsm;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.Map;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.AsSubgraph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\n\nclass CircularReferenceCheckerTests {\n\n    CircularReferenceChecker sutCircularReferenceChecker = new CircularReferenceChecker();\n\n    @DisplayName(\"Detect 3 cycles from given graph.\")\n    @Test\n    void detectCyclesTest() {\n        Graph<String, DefaultWeightedEdge> classReferencesGraph = new DefaultDirectedGraph<>(DefaultWeightedEdge.class);\n        classReferencesGraph.addVertex(\"A\");\n        classReferencesGraph.addVertex(\"B\");\n        classReferencesGraph.addVertex(\"C\");\n        classReferencesGraph.addEdge(\"A\", \"B\");\n        classReferencesGraph.addEdge(\"B\", \"C\");\n\n        Map<String, AsSubgraph<String, DefaultWeightedEdge>> cyclesForEveryVertexMap =\n                sutCircularReferenceChecker.getCycles(classReferencesGraph);\n        assertEquals(0, cyclesForEveryVertexMap.size(), \"Not expecting any circular references at this point\");\n\n        classReferencesGraph.addEdge(\"C\", \"A\");\n\n        cyclesForEveryVertexMap = sutCircularReferenceChecker.getCycles(classReferencesGraph);\n        assertEquals(1, cyclesForEveryVertexMap.size(), \"Now we expect one circular reference\");\n        assertEquals(\n                \"([A, B, C], [(A,B), (B,C), (C,A)])\",\n                cyclesForEveryVertexMap.get(\"A\").toString(),\n                \"Expected a different circular reference\");\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/dsm/DSMTest.java",
    "content": "package org.hjug.dsm;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.List;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.graph.SimpleDirectedWeightedGraph;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass DSMTest {\n\n    DSM<String, DefaultWeightedEdge> dsm = new DSM(new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class));\n\n    @BeforeEach\n    void setUp() {\n        dsm.addActivity(\"A\");\n        dsm.addActivity(\"B\");\n        dsm.addActivity(\"C\");\n        dsm.addActivity(\"D\");\n\n        dsm.addDependency(\"A\", \"B\", 1);\n        dsm.addDependency(\"B\", \"C\", 2);\n        dsm.addDependency(\"C\", \"D\", 3);\n        dsm.addDependency(\"B\", \"A\", 6); // Adding a cycle\n        dsm.addDependency(\"C\", \"A\", 5); // Adding a cycle\n        dsm.addDependency(\"D\", \"A\", 4); // Adding a cycle\n\n        /*\n              D C B A\n            D - 0 0 4\n            C 3 - 0 5\n            B 0 2 - 6\n            A 0 0 1 -\n        */\n\n        dsm.addActivity(\"E\");\n        dsm.addActivity(\"F\");\n        dsm.addActivity(\"G\");\n        dsm.addActivity(\"H\");\n        dsm.addDependency(\"D\", \"C\", 2);\n        dsm.addDependency(\"A\", \"H\", 7);\n        dsm.addDependency(\"E\", \"C\", 9);\n        dsm.addDependency(\"E\", \"H\", 2);\n        dsm.addDependency(\"G\", \"E\", 2);\n        dsm.addDependency(\"H\", \"D\", 9);\n        dsm.addDependency(\"H\", \"G\", 5);\n\n        //                dsm.printDSM();\n    }\n\n    @Test\n    void optimalBackwardEdgeToRemove() {\n        // Identify which edge above the diagonal should be removed first\n        DefaultWeightedEdge edge = dsm.getFirstLowestWeightEdgeAboveDiagonalToRemove();\n        assertEquals(\"(D : C)\", edge.toString());\n    }\n\n    @Test\n    void optimalBackwardEdgeToRemoveWithWeightOfOne() {\n        DSM<String, DefaultWeightedEdge> dsm2 = new DSM<>(new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class));\n        dsm2.addActivity(\"A\");\n        dsm2.addActivity(\"B\");\n        dsm2.addActivity(\"C\");\n\n        dsm2.addDependency(\"A\", \"B\", 1);\n        dsm2.addDependency(\"B\", \"C\", 1);\n        dsm2.addDependency(\"B\", \"A\", 1);\n        dsm2.addDependency(\"C\", \"A\", 1);\n\n        // Identify which edge above the diagonal should be removed first\n        DefaultWeightedEdge edge = dsm2.getFirstLowestWeightEdgeAboveDiagonalToRemove();\n        assertEquals(\"(C : A)\", edge.toString());\n    }\n\n    @Test\n    void minWeightBackwardEdges() {\n        // Identify which edge above the diagonal in the set of cycles should be removed first\n        List<DefaultWeightedEdge> edges = dsm.getMinimumWeightEdgesAboveDiagonal();\n        assertEquals(2, edges.size());\n        assertEquals(\"(D : C)\", edges.get(0).toString());\n        assertEquals(\"(E : H)\", edges.get(1).toString());\n    }\n\n    @Test\n    void edgesAboveDiagonal() {\n        // Identify edges above the diagonal\n        List<DefaultWeightedEdge> edges = dsm.getEdgesAboveDiagonal();\n        assertEquals(5, edges.size());\n        assertEquals(\"(D : C)\", edges.get(0).toString());\n        assertEquals(\"(D : A)\", edges.get(1).toString());\n        assertEquals(\"(C : A)\", edges.get(2).toString());\n        assertEquals(\"(B : A)\", edges.get(3).toString());\n        assertEquals(\"(E : H)\", edges.get(4).toString());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/dsm/EdgeRemovalCalculatorTest.java",
    "content": "package org.hjug.dsm;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.List;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.graph.SimpleDirectedWeightedGraph;\nimport org.junit.jupiter.api.Test;\n\npublic class EdgeRemovalCalculatorTest {\n\n    DSM<String, DefaultWeightedEdge> dsm;\n\n    @Test\n    void getImpactOfEdgesAboveDiagonalIfRemoved() {\n        SimpleDirectedWeightedGraph<String, DefaultWeightedEdge> graph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        dsm = new DSM<>(graph);\n        dsm.addActivity(\"A\");\n        dsm.addActivity(\"B\");\n        dsm.addActivity(\"C\");\n        dsm.addActivity(\"D\");\n\n        // Cycle 1\n        dsm.addDependency(\"A\", \"B\", 1);\n        dsm.addDependency(\"B\", \"C\", 2);\n        dsm.addDependency(\"C\", \"D\", 3);\n        dsm.addDependency(\"B\", \"A\", 6); // Adding a cycle\n        dsm.addDependency(\"C\", \"A\", 5); // Adding a cycle\n        dsm.addDependency(\"D\", \"A\", 4); // Adding a cycle\n\n        // Cycle 2\n        dsm.addActivity(\"E\");\n        dsm.addActivity(\"F\");\n        dsm.addActivity(\"G\");\n        dsm.addActivity(\"H\");\n        dsm.addDependency(\"E\", \"F\", 2);\n        dsm.addDependency(\"F\", \"G\", 7);\n        dsm.addDependency(\"G\", \"H\", 9);\n        dsm.addDependency(\"H\", \"E\", 9); // create cycle\n\n        dsm.addDependency(\"A\", \"E\", 9);\n        dsm.addDependency(\"E\", \"A\", 3); // create cycle between cycles\n\n        EdgeRemovalCalculator edgeRemovalCalculator = new EdgeRemovalCalculator(graph, dsm);\n\n        List<EdgeToRemoveInfo> infos = edgeRemovalCalculator.getImpactOfEdgesAboveDiagonalIfRemoved(50);\n        assertEquals(5, infos.size());\n\n        assertEquals(\"(D : A)\", infos.get(0).getEdge().toString());\n        assertEquals(3, infos.get(0).getNewCycleCount());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/dsm/OptimalBackEdgeRemoverTest.java",
    "content": "package org.hjug.dsm;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.ArrayList;\nimport java.util.Set;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.jgrapht.graph.SimpleDirectedWeightedGraph;\nimport org.junit.jupiter.api.Test;\n\nclass OptimalBackEdgeRemoverTest {\n\n    @Test\n    void noOptimalEdge() {\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        classReferencesGraph.addVertex(\"A\");\n        classReferencesGraph.addVertex(\"B\");\n        classReferencesGraph.addVertex(\"C\");\n        classReferencesGraph.addEdge(\"A\", \"B\");\n        classReferencesGraph.addEdge(\"B\", \"C\");\n\n        OptimalBackEdgeRemover<String, DefaultWeightedEdge> remover = new OptimalBackEdgeRemover(classReferencesGraph);\n        Set<DefaultWeightedEdge> optimalEdges = remover.findOptimalBackEdgesToRemove();\n\n        assertTrue(optimalEdges.isEmpty());\n    }\n\n    @Test\n    void oneBackEdge() {\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        classReferencesGraph.addVertex(\"A\");\n        classReferencesGraph.addVertex(\"B\");\n        classReferencesGraph.addVertex(\"C\");\n        classReferencesGraph.addEdge(\"A\", \"B\");\n        classReferencesGraph.addEdge(\"B\", \"C\");\n        classReferencesGraph.addEdge(\"C\", \"A\");\n\n        OptimalBackEdgeRemover<String, DefaultWeightedEdge> remover = new OptimalBackEdgeRemover(classReferencesGraph);\n        Set<DefaultWeightedEdge> optimalEdges = remover.findOptimalBackEdgesToRemove();\n\n        // all are considered back edges since this is a cycle\n        assertEquals(3, optimalEdges.size());\n    }\n\n    @Test\n    void twoBackEdges() {\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        classReferencesGraph.addVertex(\"A\");\n        classReferencesGraph.addVertex(\"B\");\n        classReferencesGraph.addVertex(\"C\");\n        classReferencesGraph.addVertex(\"D\");\n        classReferencesGraph.addEdge(\"A\", \"B\");\n        classReferencesGraph.addEdge(\"B\", \"C\");\n        classReferencesGraph.addEdge(\"C\", \"D\");\n        classReferencesGraph.addEdge(\"C\", \"A\"); // back edge\n        classReferencesGraph.addEdge(\"D\", \"A\"); // back edge\n\n        OptimalBackEdgeRemover<String, DefaultWeightedEdge> remover = new OptimalBackEdgeRemover(classReferencesGraph);\n        Set<DefaultWeightedEdge> optimalEdges = remover.findOptimalBackEdgesToRemove();\n\n        assertEquals(2, optimalEdges.size());\n    }\n\n    @Test\n    void multi() {\n        Graph<String, DefaultWeightedEdge> classReferencesGraph =\n                new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        classReferencesGraph.addVertex(\"A\");\n        classReferencesGraph.addVertex(\"B\");\n        classReferencesGraph.addVertex(\"C\");\n        classReferencesGraph.addVertex(\"D\");\n\n        // Cycle 1\n        classReferencesGraph.addEdge(\"A\", \"B\");\n        classReferencesGraph.addEdge(\"B\", \"C\");\n        classReferencesGraph.addEdge(\"C\", \"D\");\n        classReferencesGraph.addEdge(\"B\", \"A\"); // Adding a cycle\n        classReferencesGraph.addEdge(\"C\", \"A\"); // Adding a cycle\n        classReferencesGraph.addEdge(\"D\", \"A\"); // Adding a cycle\n\n        // Cycle 2\n        classReferencesGraph.addVertex(\"E\");\n        classReferencesGraph.addVertex(\"F\");\n        classReferencesGraph.addVertex(\"G\");\n        classReferencesGraph.addVertex(\"H\");\n        classReferencesGraph.addEdge(\"E\", \"F\");\n        classReferencesGraph.addEdge(\"F\", \"G\");\n        classReferencesGraph.addEdge(\"G\", \"H\");\n        classReferencesGraph.addEdge(\"H\", \"E\"); // create cycle\n\n        classReferencesGraph.addEdge(\"A\", \"E\");\n        classReferencesGraph.addEdge(\"E\", \"A\"); // create cycle between cycles\n\n        OptimalBackEdgeRemover<String, DefaultWeightedEdge> remover = new OptimalBackEdgeRemover(classReferencesGraph);\n        Set<DefaultWeightedEdge> optimalEdges = remover.findOptimalBackEdgesToRemove();\n\n        assertEquals(1, optimalEdges.size());\n        assertEquals(\"(A : B)\", new ArrayList<>(optimalEdges).get(0).toString());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/SuperTypeTokenTest.java",
    "content": "package org.hjug.feedback;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.List;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass SuperTypeTokenTest {\n\n    SuperTypeToken<DefaultWeightedEdge> token;\n\n    @BeforeEach\n    void setUp() {\n        token = new SuperTypeToken<>() {};\n    }\n\n    @Test\n    void getType() {\n        assertEquals(\n                \"class org.jgrapht.graph.DefaultWeightedEdge\", token.getType().toString());\n    }\n\n    @Test\n    void getGenericType() {\n        SuperTypeToken<List<String>> genericToken = new SuperTypeToken<>() {};\n        assertEquals(\"java.util.List<java.lang.String>\", genericToken.getType().toString());\n        assertEquals(List.class, genericToken.getClassFromTypeToken());\n    }\n\n    @Test\n    void getClassFromType() {\n        assertEquals(DefaultWeightedEdge.class, token.getClassFromTypeToken());\n    }\n\n    @Test\n    void typeWithGenericParameter() {\n        assertEquals(DefaultWeightedEdge.class, new GenericTestClass<>(token).getTypeTokenClass());\n    }\n}\n\nclass GenericTestClass<T> {\n    SuperTypeToken<T> typeToken;\n\n    public GenericTestClass(SuperTypeToken<T> token) {\n        this.typeToken = token;\n    }\n\n    public Class<T> getTypeTokenClass() {\n        return typeToken.getClassFromTypeToken();\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/arc/approximate/FeedbackArcSetBenchmarkTest.java",
    "content": "package org.hjug.feedback.arc.approximate;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ThreadLocalRandom;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Benchmark tests for performance evaluation\n */\nclass FeedbackArcSetBenchmarkTest {\n\n    @Test\n    @DisplayName(\"Benchmark: Dense graphs with varying sizes\")\n    void benchmarkDenseGraphs() {\n        int[] sizes = {10, 25, 50, 100};\n\n        System.out.println(\"=== Dense Graph Benchmark ===\");\n        System.out.printf(\"%-10s %-15s %-15s %-15s %-15s%n\", \"Size\", \"Vertices\", \"Edges\", \"FAS Size\", \"Time (ms)\");\n\n        for (int size : sizes) {\n            Graph<String, DefaultEdge> graph = createDenseGraph(size);\n\n            long startTime = System.currentTimeMillis();\n            FeedbackArcSetSolver<String, DefaultEdge> solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n            long endTime = System.currentTimeMillis();\n\n            System.out.printf(\n                    \"%-10d %-15d %-15d %-15d %-15d%n\",\n                    size,\n                    graph.vertexSet().size(),\n                    graph.edgeSet().size(),\n                    result.getFeedbackArcCount(),\n                    endTime - startTime);\n        }\n    }\n\n    @Test\n    @DisplayName(\"Benchmark: Sparse graphs with varying sizes\")\n    void benchmarkSparseGraphs() {\n        int[] sizes = {50, 100, 200, 500, 1000, 1500};\n\n        System.out.println(\"=== Sparse Graph Benchmark ===\");\n        System.out.printf(\"%-10s %-15s %-15s %-15s %-15s%n\", \"Size\", \"Vertices\", \"Edges\", \"FAS Size\", \"Time (ms)\");\n\n        for (int size : sizes) {\n            Graph<String, DefaultEdge> graph = createSparseGraph(size);\n\n            long startTime = System.currentTimeMillis();\n            FeedbackArcSetSolver<String, DefaultEdge> solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n            long endTime = System.currentTimeMillis();\n\n            System.out.printf(\n                    \"%-10d %-15d %-15d %-15d %-15d%n\",\n                    size,\n                    graph.vertexSet().size(),\n                    graph.edgeSet().size(),\n                    result.getFeedbackArcCount(),\n                    endTime - startTime);\n        }\n    }\n\n    private Graph<String, DefaultEdge> createDenseGraph(int size) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        for (int i = 0; i < size; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        List<String> vertices = new ArrayList<>(graph.vertexSet());\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        // Add edges with high probability\n        for (int i = 0; i < size; i++) {\n            for (int j = 0; j < size; j++) {\n                if (i != j && random.nextDouble() < 0.6) {\n                    graph.addEdge(vertices.get(i), vertices.get(j));\n                }\n            }\n        }\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createSparseGraph(int size) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        for (int i = 0; i < size; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        List<String> vertices = new ArrayList<>(graph.vertexSet());\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        // Add approximately 2*size edges (sparse)\n        int targetEdges = size * 2;\n        int addedEdges = 0;\n\n        while (addedEdges < targetEdges) {\n            String source = vertices.get(random.nextInt(vertices.size()));\n            String target = vertices.get(random.nextInt(vertices.size()));\n\n            if (!source.equals(target) && !graph.containsEdge(source, target)) {\n                graph.addEdge(source, target);\n                addedEdges++;\n            }\n        }\n\n        return graph;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/arc/approximate/FeedbackArcSetExample.java",
    "content": "package org.hjug.feedback.arc.approximate;\n\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\n\npublic class FeedbackArcSetExample {\n    public static void main(String[] args) {\n        // Create a directed graph with cycles\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        graph.addVertex(\"A\");\n        graph.addVertex(\"B\");\n        graph.addVertex(\"C\");\n        graph.addVertex(\"D\");\n\n        // Add edges creating cycles\n        graph.addEdge(\"A\", \"B\");\n        graph.addEdge(\"B\", \"C\");\n        graph.addEdge(\"C\", \"A\"); // Creates cycle A->B->C->A\n        graph.addEdge(\"C\", \"D\");\n        graph.addEdge(\"D\", \"A\"); // Creates cycle A->B->C->D->A\n\n        // Solve the FAS problem\n        FeedbackArcSetSolver<String, DefaultEdge> solver = new FeedbackArcSetSolver<>(graph);\n        FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n        System.out.println(\"Vertex sequence: \" + result.getVertexSequence());\n        System.out.println(\"Feedback arc count: \" + result.getFeedbackArcCount());\n        System.out.println(\"Feedback arcs: \" + result.getFeedbackArcs());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/arc/approximate/FeedbackArcSetSolverTest.java",
    "content": "package org.hjug.feedback.arc.approximate;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\n/**\n * Comprehensive unit tests for the FeedbackArcSetSolver\n */\nclass FeedbackArcSetSolverTest {\n\n    private Graph<String, DefaultEdge> graph;\n    private FeedbackArcSetSolver<String, DefaultEdge> solver;\n\n    @BeforeEach\n    void setUp() {\n        graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n    }\n\n    @Nested\n    @DisplayName(\"Basic Algorithm Tests\")\n    class BasicAlgorithmTests {\n\n        @Test\n        @DisplayName(\"Should handle empty graph\")\n        void testEmptyGraph() {\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertTrue(result.getVertexSequence().isEmpty());\n            assertTrue(result.getFeedbackArcs().isEmpty());\n            assertEquals(0, result.getFeedbackArcCount());\n        }\n\n        @Test\n        @DisplayName(\"Should handle single vertex\")\n        void testSingleVertex() {\n            graph.addVertex(\"A\");\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(1, result.getVertexSequence().size());\n            assertTrue(result.getVertexSequence().contains(\"A\"));\n            assertEquals(0, result.getFeedbackArcCount());\n        }\n\n        @Test\n        @DisplayName(\"Should handle acyclic graph\")\n        void testAcyclicGraph() {\n            // Create a simple DAG: A -> B -> C\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(0, result.getFeedbackArcCount());\n            assertEquals(3, result.getVertexSequence().size());\n        }\n\n        @Test\n        @DisplayName(\"Should handle simple cycle\")\n        void testSimpleCycle() {\n            // Create a simple cycle: A -> B -> C -> A\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            // Should break the cycle with exactly one feedback arc\n            assertEquals(1, result.getFeedbackArcCount());\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Complex Graph Tests\")\n    class ComplexGraphTests {\n\n        @Test\n        @DisplayName(\"Should handle multiple cycles\")\n        void testMultipleCycles() {\n            // Create graph with multiple overlapping cycles\n            String[] vertices = {\"A\", \"B\", \"C\", \"D\", \"E\"};\n            for (String v : vertices) {\n                graph.addVertex(v);\n            }\n\n            // Create cycles: A->B->C->A and C->D->E->C\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n            graph.addEdge(\"C\", \"D\");\n            graph.addEdge(\"D\", \"E\");\n            graph.addEdge(\"E\", \"C\");\n\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertTrue(result.getFeedbackArcCount() >= 2);\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n\n        @Test\n        @DisplayName(\"Should handle tournament graph\")\n        void testTournamentGraph() {\n            // Create a tournament (complete directed graph)\n            String[] vertices = {\"A\", \"B\", \"C\", \"D\"};\n            for (String v : vertices) {\n                graph.addVertex(v);\n            }\n\n            // Add edges to create a tournament\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"A\", \"C\");\n            graph.addEdge(\"A\", \"D\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"B\", \"D\");\n            graph.addEdge(\"C\", \"D\");\n            graph.addEdge(\"D\", \"A\"); // Creates cycles\n            graph.addEdge(\"C\", \"B\"); // Creates cycles\n\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertGraphIsAcyclicAfterRemoval(result);\n            // For tournaments, the bound should be ≤ m/2 + n/4\n            int m = graph.edgeSet().size();\n            int n = graph.vertexSet().size();\n            assertTrue(result.getFeedbackArcCount() <= m / 2 + n / 4);\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Performance Tests\")\n    class PerformanceTests {\n\n        @ParameterizedTest\n        @ValueSource(ints = {10, 50, 100})\n        @DisplayName(\"Should handle large random graphs efficiently\")\n        void testLargeRandomGraphs(int size) {\n            createRandomGraph(size, size * 2);\n\n            long startTime = System.currentTimeMillis();\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n            long endTime = System.currentTimeMillis();\n\n            assertGraphIsAcyclicAfterRemoval(result);\n\n            // Performance should be reasonable (less than 5 seconds for size 100)\n            assertTrue(endTime - startTime < 5000, \"Algorithm took too long: \" + (endTime - startTime) + \"ms\");\n        }\n\n        @Test\n        @DisplayName(\"Should verify parallel processing improves performance\")\n        void testParallelPerformanceImprovement() {\n            createRandomGraph(50, 100);\n\n            // Test with current parallel implementation\n            long startTimeParallel = System.currentTimeMillis();\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> parallelResult = solver.solve();\n            long endTimeParallel = System.currentTimeMillis();\n\n            assertGraphIsAcyclicAfterRemoval(parallelResult);\n\n            // Verify result quality meets the theoretical bound\n            int m = graph.edgeSet().size();\n            int n = graph.vertexSet().size();\n            assertTrue(parallelResult.getFeedbackArcCount() <= m / 2 + n / 4);\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Edge Cases\")\n    class EdgeCaseTests {\n\n        @Test\n        @DisplayName(\"Should handle self-loops\")\n        void testSelfLoops() {\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            // JGraphT DefaultDirectedGraph doesn't allow self-loops by default\n            // But we can test the behavior\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"A\");\n\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(1, result.getFeedbackArcCount());\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n\n        @Test\n        @DisplayName(\"Should handle disconnected components\")\n        void testDisconnectedComponents() {\n            // Component 1: A -> B -> A\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"A\");\n\n            // Component 2: C -> D (acyclic)\n            graph.addVertex(\"C\");\n            graph.addVertex(\"D\");\n            graph.addEdge(\"C\", \"D\");\n\n            // Component 3: E (isolated)\n            graph.addVertex(\"E\");\n\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(1, result.getFeedbackArcCount());\n            assertGraphIsAcyclicAfterRemoval(result);\n            assertEquals(5, result.getVertexSequence().size());\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Correctness Verification\")\n    class CorrectnessTests {\n\n        @Test\n        @DisplayName(\"Should produce valid vertex ordering\")\n        void testVertexOrderingValidity() {\n            createRandomGraph(20, 40);\n\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            // Verify all vertices are included in the sequence\n            assertEquals(graph.vertexSet().size(), result.getVertexSequence().size());\n            assertTrue(result.getVertexSequence().containsAll(graph.vertexSet()));\n\n            // Verify no duplicates\n            Set<String> uniqueVertices = new HashSet<>(result.getVertexSequence());\n            assertEquals(graph.vertexSet().size(), uniqueVertices.size());\n        }\n\n        @Test\n        @DisplayName(\"Should satisfy performance bound\")\n        void testPerformanceBound() {\n            createRandomGraph(30, 60);\n\n            solver = new FeedbackArcSetSolver<>(graph);\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            int m = graph.edgeSet().size();\n            int n = graph.vertexSet().size();\n            int bound = m / 2 + n / 4;\n\n            assertTrue(\n                    result.getFeedbackArcCount() <= bound,\n                    String.format(\"FAS size %d exceeds bound %d\", result.getFeedbackArcCount(), bound));\n        }\n    }\n\n    // Helper methods\n\n    private void createRandomGraph(int vertexCount, int edgeCount) {\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        // Add vertices\n        for (int i = 0; i < vertexCount; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        List<String> vertices = new ArrayList<>(graph.vertexSet());\n\n        // Add random edges\n        int addedEdges = 0;\n        while (addedEdges < edgeCount) {\n            String source = vertices.get(random.nextInt(vertices.size()));\n            String target = vertices.get(random.nextInt(vertices.size()));\n\n            if (!source.equals(target) && !graph.containsEdge(source, target)) {\n                graph.addEdge(source, target);\n                addedEdges++;\n            }\n        }\n    }\n\n    private void assertGraphIsAcyclicAfterRemoval(FeedbackArcSetResult<String, DefaultEdge> result) {\n        // Create a copy of the graph without feedback arcs\n        Graph<String, DefaultEdge> testGraph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add all vertices\n        for (String vertex : graph.vertexSet()) {\n            testGraph.addVertex(vertex);\n        }\n\n        // Add all edges except feedback arcs\n        for (DefaultEdge edge : graph.edgeSet()) {\n            if (!result.getFeedbackArcs().contains(edge)) {\n                String source = graph.getEdgeSource(edge);\n                String target = graph.getEdgeTarget(edge);\n                testGraph.addEdge(source, target);\n            }\n        }\n\n        // Verify the resulting graph is acyclic\n        CycleDetector<String, DefaultEdge> cycleDetector = new CycleDetector<>(testGraph);\n        assertFalse(cycleDetector.detectCycles(), \"Graph should be acyclic after removing feedback arcs\");\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/arc/exact/MinimumFeedbackArcSetBenchmarkTest.java",
    "content": "package org.hjug.feedback.arc.exact;\n\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.IntStream;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Performance benchmark tests for the algorithm [2]\n */\nclass MinimumFeedbackArcSetBenchmarkTest {\n\n    @Test\n    @DisplayName(\"Benchmark: Various graph sizes and densities\")\n    void benchmarkGraphSizes() {\n        int[] sizes = {20, 50, 100};\n        double[] densities = {0.1, 0.3, 0.5};\n\n        System.out.println(\"=== Minimum Feedback Arc Set Benchmark ===\");\n        System.out.printf(\n                \"%-10s %-15s %-15s %-15s %-15s %-15s%n\",\n                \"Size\", \"Density\", \"Vertices\", \"Edges\", \"FAS Size\", \"Time (ms)\");\n\n        for (int size : sizes) {\n            for (double density : densities) {\n                Graph<String, DefaultEdge> graph = createRandomGraph(size, density);\n\n                long startTime = System.currentTimeMillis();\n                MinimumFeedbackArcSetSolver<String, DefaultEdge> solver =\n                        new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n                FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n                long endTime = System.currentTimeMillis();\n\n                System.out.printf(\n                        \"%-10d %-15.1f %-15d %-15d %-15d %-15d%n\",\n                        size,\n                        density,\n                        graph.vertexSet().size(),\n                        graph.edgeSet().size(),\n                        result.size(),\n                        endTime - startTime);\n            }\n        }\n    }\n\n    private Graph<String, DefaultEdge> createRandomGraph(int size, double density) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices using parallel streams [18]\n        IntStream.range(0, size).forEach(i -> graph.addVertex(\"V\" + i));\n\n        List<String> vertices = new ArrayList<>(graph.vertexSet());\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        int maxEdges = size * (size - 1);\n        int targetEdges = (int) (maxEdges * density);\n\n        int addedEdges = 0;\n        while (addedEdges < targetEdges) {\n            String source = vertices.get(random.nextInt(vertices.size()));\n            String target = vertices.get(random.nextInt(vertices.size()));\n\n            if (!source.equals(target) && !graph.containsEdge(source, target)) {\n                graph.addEdge(source, target);\n                addedEdges++;\n            }\n        }\n\n        return graph;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/arc/exact/MinimumFeedbackArcSetExample.java",
    "content": "package org.hjug.feedback.arc.exact;\n\nimport java.util.Map;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\n\npublic class MinimumFeedbackArcSetExample {\n    public static void main(String[] args) {\n        // Create a directed graph with cycles\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        graph.addVertex(\"A\");\n        graph.addVertex(\"B\");\n        graph.addVertex(\"C\");\n        graph.addVertex(\"D\");\n\n        // Add edges creating cycles\n        DefaultEdge e1 = graph.addEdge(\"A\", \"B\");\n        DefaultEdge e2 = graph.addEdge(\"B\", \"C\");\n        DefaultEdge e3 = graph.addEdge(\"C\", \"A\"); // Creates cycle A->B->C->A\n        DefaultEdge e4 = graph.addEdge(\"C\", \"D\");\n        DefaultEdge e5 = graph.addEdge(\"D\", \"A\"); // Creates cycle A->B->C->D->A\n\n        // Define edge weights (optional)\n        Map<DefaultEdge, Double> weights = Map.of(e1, 1.0, e2, 2.0, e3, 1.5, e4, 1.0, e5, 1.0);\n\n        // Solve the minimum feedback arc set problem\n        MinimumFeedbackArcSetSolver<String, DefaultEdge> solver =\n                new MinimumFeedbackArcSetSolver<>(graph, weights, new SuperTypeToken<>() {});\n        FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n        System.out.println(\"Minimum feedback arc set: \" + result.getFeedbackArcSet());\n        System.out.println(\"Objective value: \" + result.getObjectiveValue());\n        System.out.println(\"Solution size: \" + result.size());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/arc/exact/MinimumFeedbackArcSetSolverTest.java",
    "content": "package org.hjug.feedback.arc.exact;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.IntStream;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.parallel.Execution;\nimport org.junit.jupiter.api.parallel.ExecutionMode;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\n/**\n * Comprehensive unit tests for the MinimumFeedbackArcSetSolver [15]\n */\n@Execution(ExecutionMode.CONCURRENT)\nclass MinimumFeedbackArcSetSolverTest {\n\n    private Graph<String, DefaultEdge> graph;\n    private MinimumFeedbackArcSetSolver<String, DefaultEdge> solver;\n\n    @BeforeEach\n    void setUp() {\n        graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n    }\n\n    @Nested\n    @DisplayName(\"Basic Algorithm Tests\")\n    class BasicAlgorithmTests {\n\n        @Test\n        @DisplayName(\"Should handle empty graph\")\n        void testEmptyGraph() {\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertTrue(result.getFeedbackArcSet().isEmpty());\n            assertEquals(0.0, result.getObjectiveValue());\n        }\n\n        @Test\n        @DisplayName(\"Should handle single vertex\")\n        void testSingleVertex() {\n            graph.addVertex(\"A\");\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(0, result.size());\n        }\n\n        @Test\n        @DisplayName(\"Should handle acyclic graph\")\n        void testAcyclicGraph() {\n            // Create a simple DAG: A -> B -> C [15]\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(0, result.size());\n        }\n\n        @Test\n        @DisplayName(\"Should handle simple cycle\")\n        void testSimpleCycle() {\n            // Create a simple cycle: A -> B -> C -> A [2]\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            // Should break the cycle with exactly one arc\n            assertEquals(1, result.size());\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n\n        @Test\n        @DisplayName(\"Should handle self-loop\")\n        @Disabled(\"Does not pass, but I (JRB) am not concerned about this case\")\n        void testSelfLoop() {\n            graph.addVertex(\"A\");\n            DefaultEdge selfLoop = graph.addEdge(\"A\", \"A\");\n\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(1, result.size());\n            assertTrue(result.getFeedbackArcSet().contains(selfLoop));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Complex Graph Tests\")\n    class ComplexGraphTests {\n\n        @Test\n        @DisplayName(\"Should handle multiple cycles\")\n        void testMultipleCycles() {\n            // Create graph with multiple overlapping cycles [2]\n            String[] vertices = {\"A\", \"B\", \"C\", \"D\", \"E\"};\n            for (String v : vertices) {\n                graph.addVertex(v);\n            }\n\n            // Create cycles: A->B->C->A and C->D->E->C\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n            graph.addEdge(\"C\", \"D\");\n            graph.addEdge(\"D\", \"E\");\n            graph.addEdge(\"E\", \"C\");\n\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertTrue(result.size() >= 2);\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n\n        @Test\n        @DisplayName(\"Should handle disconnected components\")\n        void testDisconnectedComponents() {\n            // Component 1: A -> B -> A\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"A\");\n\n            // Component 2: C -> D (acyclic)\n            graph.addVertex(\"C\");\n            graph.addVertex(\"D\");\n            graph.addEdge(\"C\", \"D\");\n\n            // Component 3: E (isolated)\n            graph.addVertex(\"E\");\n\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(1, result.size());\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n\n        @Test\n        @DisplayName(\"Should handle weighted edges\")\n        void testWeightedEdges() {\n            // Create a cycle with different edge weights\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            DefaultEdge e1 = graph.addEdge(\"A\", \"B\");\n            DefaultEdge e2 = graph.addEdge(\"B\", \"C\");\n            DefaultEdge e3 = graph.addEdge(\"C\", \"A\");\n\n            Map<DefaultEdge, Double> weights = Map.of(e1, 1.0, e2, 10.0, e3, 1.0);\n\n            solver = new MinimumFeedbackArcSetSolver<>(graph, weights, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(1, result.size());\n            // Should prefer removing lower weight edges\n            assertFalse(result.getFeedbackArcSet().contains(e2));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Performance Tests\")\n    class PerformanceTests {\n\n        @ParameterizedTest\n        @ValueSource(ints = {10, 25, 50})\n        @DisplayName(\"Should handle random graphs efficiently\")\n        void testRandomGraphPerformance(int size) {\n            createRandomGraph(size, size * 2);\n\n            long startTime = System.currentTimeMillis();\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n            long endTime = System.currentTimeMillis();\n\n            // Performance should be reasonable [2]\n            assertTrue(endTime - startTime < 10000, \"Algorithm took too long: \" + (endTime - startTime) + \"ms\");\n\n            if (hasCycles()) {\n                assertGraphIsAcyclicAfterRemoval(result);\n            }\n        }\n\n        @Test\n        @DisplayName(\"Should utilize parallel processing effectively\")\n        void testParallelProcessing() {\n            createRandomGraph(30, 60);\n\n            long startTime = System.currentTimeMillis();\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n            long endTime = System.currentTimeMillis();\n\n            assertTrue(endTime - startTime < 15000);\n            if (hasCycles()) {\n                assertGraphIsAcyclicAfterRemoval(result);\n            }\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Correctness Tests\")\n    class CorrectnessTests {\n\n        @Test\n        @DisplayName(\"Should maintain optimality properties\")\n        void testOptimalityProperties() {\n            createRandomGraph(15, 30);\n\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            // Solution should be minimal and make graph acyclic [2]\n            if (hasCycles()) {\n                assertGraphIsAcyclicAfterRemoval(result);\n                assertTrue(result.size() > 0);\n            }\n        }\n\n        @Test\n        @DisplayName(\"Should handle edge cases correctly\")\n        void testEdgeCases() {\n            // Triangle with all edges having same weight\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n\n            solver = new MinimumFeedbackArcSetSolver<>(graph, null, new SuperTypeToken<>() {});\n            FeedbackArcSetResult<String, DefaultEdge> result = solver.solve();\n\n            assertEquals(1, result.size());\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n    }\n\n    // Helper methods\n\n    private void createRandomGraph(int vertexCount, int edgeCount) {\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        // Add vertices [18]\n        IntStream.range(0, vertexCount).forEach(i -> graph.addVertex(\"V\" + i));\n        List<String> vertices = new ArrayList<>(graph.vertexSet());\n\n        // Add random edges\n        int addedEdges = 0;\n        while (addedEdges < edgeCount && addedEdges < vertexCount * (vertexCount - 1)) {\n            String source = vertices.get(random.nextInt(vertices.size()));\n            String target = vertices.get(random.nextInt(vertices.size()));\n\n            if (!source.equals(target) && !graph.containsEdge(source, target)) {\n                graph.addEdge(source, target);\n                addedEdges++;\n            }\n        }\n    }\n\n    private boolean hasCycles() {\n        CycleDetector<String, DefaultEdge> cycleDetector = new CycleDetector<>(graph);\n        return cycleDetector.detectCycles();\n    }\n\n    private void assertGraphIsAcyclicAfterRemoval(FeedbackArcSetResult<String, DefaultEdge> result) {\n        // Create a copy of the graph without feedback arcs [12]\n        Graph<String, DefaultEdge> testGraph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        Set<String> resultEdgesAsStrings = new HashSet<>();\n        result.getFeedbackArcSet().forEach(edge -> resultEdgesAsStrings.add(edge.toString()));\n\n        // Add all vertices\n        graph.vertexSet().forEach(testGraph::addVertex);\n\n        // Add edges not in feedback arc set\n        graph.edgeSet().stream()\n                .filter(edge -> !resultEdgesAsStrings.contains(edge.toString()))\n                .forEach(edge -> {\n                    String source = graph.getEdgeSource(edge);\n                    String target = graph.getEdgeTarget(edge);\n                    testGraph.addEdge(source, target);\n                });\n\n        // Verify the resulting graph is acyclic [12][16]\n        CycleDetector<String, DefaultEdge> cycleDetector = new CycleDetector<>(testGraph);\n        assertFalse(cycleDetector.detectCycles(), \"Graph should be acyclic after removing feedback arcs\");\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/arc/pageRank/PageRankFASExample.java",
    "content": "package org.hjug.feedback.arc.pageRank;\n\nimport java.util.Set;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\n\n/**\n * Example usage of the PageRankFAS algorithm\n * Demonstrates how to use the algorithm with different types of graphs\n */\npublic class PageRankFASExample {\n\n    public static void main(String[] args) {\n        System.out.println(\"PageRankFAS Algorithm Examples\");\n        System.out.println(\"===============================\");\n\n        // Example 1: Simple cycle\n        System.out.println(\"\\n1. Simple Cycle Example:\");\n        demonstrateSimpleCycle();\n\n        // Example 2: Multiple cycles\n        System.out.println(\"\\n2. Multiple Cycles Example:\");\n        demonstrateMultipleCycles();\n\n        // Example 3: Complex graph with nested cycles\n        System.out.println(\"\\n3. Complex Graph Example:\");\n        demonstrateComplexGraph();\n\n        // Example 4: Performance comparison\n        System.out.println(\"\\n4. Performance Comparison:\");\n        demonstratePerformanceComparison();\n\n        // Example 5: Custom PageRank iterations\n        System.out.println(\"\\n5. Custom PageRank Iterations:\");\n        demonstrateCustomIterations();\n    }\n\n    /**\n     * Demonstrate PageRankFAS on a simple 3-node cycle\n     */\n    private static void demonstrateSimpleCycle() {\n        // Create a simple cycle: A -> B -> C -> A\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        graph.addVertex(\"A\");\n        graph.addVertex(\"B\");\n        graph.addVertex(\"C\");\n\n        DefaultEdge e1 = graph.addEdge(\"A\", \"B\");\n        DefaultEdge e2 = graph.addEdge(\"B\", \"C\");\n        DefaultEdge e3 = graph.addEdge(\"C\", \"A\");\n\n        System.out.println(\"Original graph: A -> B -> C -> A\");\n        System.out.println(\"Edges: \" + graph.edgeSet().size());\n        System.out.println(\"Vertices: \" + graph.vertexSet().size());\n\n        // Apply PageRankFAS\n        PageRankFAS<String, DefaultEdge> pageRankFAS = new PageRankFAS<>(graph, new SuperTypeToken<>() {});\n        Set<DefaultEdge> feedbackArcSet = pageRankFAS.computeFeedbackArcSet();\n\n        System.out.println(\"Feedback Arc Set size: \" + feedbackArcSet.size());\n        System.out.println(\"FAS edges: \" + feedbackArcSet);\n\n        // Verify the result\n        verifyAcyclicity(graph, feedbackArcSet);\n    }\n\n    /**\n     * Demonstrate PageRankFAS on a graph with multiple cycles\n     */\n    private static void demonstrateMultipleCycles() {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // First cycle: A -> B -> C -> A\n        graph.addVertex(\"A\");\n        graph.addVertex(\"B\");\n        graph.addVertex(\"C\");\n        graph.addEdge(\"A\", \"B\");\n        graph.addEdge(\"B\", \"C\");\n        graph.addEdge(\"C\", \"A\");\n\n        // Second cycle: D -> E -> F -> D\n        graph.addVertex(\"D\");\n        graph.addVertex(\"E\");\n        graph.addVertex(\"F\");\n        graph.addEdge(\"D\", \"E\");\n        graph.addEdge(\"E\", \"F\");\n        graph.addEdge(\"F\", \"D\");\n\n        // Connect the cycles\n        graph.addEdge(\"C\", \"D\");\n\n        // Add a larger cycle: A -> B -> E -> F -> A\n        graph.addEdge(\"B\", \"E\");\n        graph.addEdge(\"F\", \"A\");\n\n        System.out.println(\"Graph with multiple interconnected cycles\");\n        System.out.println(\"Edges: \" + graph.edgeSet().size());\n        System.out.println(\"Vertices: \" + graph.vertexSet().size());\n\n        // Apply PageRankFAS\n        PageRankFAS<String, DefaultEdge> pageRankFAS = new PageRankFAS<>(graph, new SuperTypeToken<>() {});\n        long startTime = System.currentTimeMillis();\n        Set<DefaultEdge> feedbackArcSet = pageRankFAS.computeFeedbackArcSet();\n        long endTime = System.currentTimeMillis();\n\n        System.out.println(\"Feedback Arc Set size: \" + feedbackArcSet.size());\n        System.out.println(\"Computation time: \" + (endTime - startTime) + \"ms\");\n\n        verifyAcyclicity(graph, feedbackArcSet);\n    }\n\n    /**\n     * Demonstrate PageRankFAS on a complex graph\n     */\n    private static void demonstrateComplexGraph() {\n        Graph<String, DefaultEdge> graph = createComplexTestGraph();\n\n        System.out.println(\"Complex graph with nested and overlapping cycles\");\n        System.out.println(\"Edges: \" + graph.edgeSet().size());\n        System.out.println(\"Vertices: \" + graph.vertexSet().size());\n\n        // Apply PageRankFAS with timing\n        PageRankFAS<String, DefaultEdge> pageRankFAS = new PageRankFAS<>(graph, new SuperTypeToken<>() {});\n        long startTime = System.currentTimeMillis();\n        Set<DefaultEdge> feedbackArcSet = pageRankFAS.computeFeedbackArcSet();\n        long endTime = System.currentTimeMillis();\n\n        System.out.println(\"Feedback Arc Set size: \" + feedbackArcSet.size());\n        System.out.println(\"Computation time: \" + (endTime - startTime) + \"ms\");\n        System.out.println(\"FAS ratio: \"\n                + String.format(\n                        \"%.2f%%\",\n                        100.0 * feedbackArcSet.size() / graph.edgeSet().size()));\n\n        verifyAcyclicity(graph, feedbackArcSet);\n    }\n\n    /**\n     * Compare performance with different graph sizes\n     */\n    private static void demonstratePerformanceComparison() {\n        int[] graphSizes = {50, 100, 200};\n\n        System.out.println(\"Performance comparison on different graph sizes:\");\n        System.out.println(\"Size\\tEdges\\tFAS Size\\tTime (ms)\\tFAS Ratio\");\n        System.out.println(\"----\\t-----\\t--------\\t---------\\t---------\");\n\n        for (int size : graphSizes) {\n            Graph<String, DefaultEdge> graph = createRandomGraph(size, size * 2);\n\n            PageRankFAS<String, DefaultEdge> pageRankFAS = new PageRankFAS<>(graph, new SuperTypeToken<>() {});\n            long startTime = System.currentTimeMillis();\n            Set<DefaultEdge> feedbackArcSet = pageRankFAS.computeFeedbackArcSet();\n            long endTime = System.currentTimeMillis();\n\n            double fasRatio = 100.0 * feedbackArcSet.size() / graph.edgeSet().size();\n\n            System.out.printf(\n                    \"%d\\t%d\\t%d\\t\\t%d\\t\\t%.2f%%\\n\",\n                    size, graph.edgeSet().size(), feedbackArcSet.size(), (endTime - startTime), fasRatio);\n        }\n    }\n\n    /**\n     * Demonstrate the effect of different PageRank iteration counts\n     */\n    private static void demonstrateCustomIterations() {\n        Graph<String, DefaultEdge> graph = createComplexTestGraph();\n        int[] iterations = {1, 3, 5, 10, 20};\n\n        System.out.println(\"Effect of PageRank iterations on FAS quality:\");\n        System.out.println(\"Iterations\\tFAS Size\\tTime (ms)\");\n        System.out.println(\"----------\\t--------\\t---------\");\n\n        for (int iter : iterations) {\n            Graph<String, DefaultEdge> testGraph = copyGraph(graph);\n\n            PageRankFAS<String, DefaultEdge> pageRankFAS =\n                    new PageRankFAS<>(testGraph, iter, new SuperTypeToken<>() {});\n\n            long startTime = System.currentTimeMillis();\n            Set<DefaultEdge> feedbackArcSet = pageRankFAS.computeFeedbackArcSet();\n            long endTime = System.currentTimeMillis();\n\n            System.out.printf(\"%d\\t\\t%d\\t\\t%d\\n\", iter, feedbackArcSet.size(), (endTime - startTime));\n        }\n    }\n\n    /**\n     * Create a complex test graph with various cycle structures\n     */\n    private static Graph<String, DefaultEdge> createComplexTestGraph() {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Create vertices\n        for (int i = 0; i < 15; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        // Create various cycle patterns\n\n        // Triangle cycles\n        graph.addEdge(\"V0\", \"V1\");\n        graph.addEdge(\"V1\", \"V2\");\n        graph.addEdge(\"V2\", \"V0\");\n\n        graph.addEdge(\"V3\", \"V4\");\n        graph.addEdge(\"V4\", \"V5\");\n        graph.addEdge(\"V5\", \"V3\");\n\n        // Square cycle\n        graph.addEdge(\"V6\", \"V7\");\n        graph.addEdge(\"V7\", \"V8\");\n        graph.addEdge(\"V8\", \"V9\");\n        graph.addEdge(\"V9\", \"V6\");\n\n        // Overlapping cycles\n        graph.addEdge(\"V2\", \"V6\"); // Connect triangle to square\n        graph.addEdge(\"V8\", \"V0\"); // Create larger cycle\n\n        // Additional complexity\n        graph.addEdge(\"V10\", \"V11\");\n        graph.addEdge(\"V11\", \"V12\");\n        graph.addEdge(\"V12\", \"V13\");\n        graph.addEdge(\"V13\", \"V14\");\n        graph.addEdge(\"V14\", \"V10\"); // Pentagon cycle\n\n        // Connect to main component\n        graph.addEdge(\"V5\", \"V10\");\n        graph.addEdge(\"V12\", \"V3\");\n\n        return graph;\n    }\n\n    /**\n     * Create a random graph for testing\n     */\n    private static Graph<String, DefaultEdge> createRandomGraph(int numVertices, int numEdges) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        for (int i = 0; i < numVertices; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        // Add random edges\n        java.util.Random random = new java.util.Random(42); // Fixed seed for reproducibility\n        java.util.List<String> vertices = new java.util.ArrayList<>(graph.vertexSet());\n\n        int edgesAdded = 0;\n        int attempts = 0;\n        while (edgesAdded < numEdges && attempts < numEdges * 3) {\n            String source = vertices.get(random.nextInt(vertices.size()));\n            String target = vertices.get(random.nextInt(vertices.size()));\n\n            if (!source.equals(target) && !graph.containsEdge(source, target)) {\n                graph.addEdge(source, target);\n                edgesAdded++;\n            }\n            attempts++;\n        }\n\n        return graph;\n    }\n\n    /**\n     * Copy a graph\n     */\n    private static Graph<String, DefaultEdge> copyGraph(Graph<String, DefaultEdge> original) {\n        Graph<String, DefaultEdge> copy = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        original.vertexSet().forEach(copy::addVertex);\n\n        // Add edges\n        original.edgeSet().forEach(edge -> {\n            String source = original.getEdgeSource(edge);\n            String target = original.getEdgeTarget(edge);\n            copy.addEdge(source, target);\n        });\n\n        return copy;\n    }\n\n    /**\n     * Verify that removing the FAS makes the graph acyclic\n     */\n    private static void verifyAcyclicity(Graph<String, DefaultEdge> originalGraph, Set<DefaultEdge> feedbackArcSet) {\n        Graph<String, DefaultEdge> testGraph = copyGraph(originalGraph);\n\n        // Remove FAS edges\n        feedbackArcSet.forEach(testGraph::removeEdge);\n\n        // Check if acyclic\n        PageRankFAS<String, DefaultEdge> verifier = new PageRankFAS<>(testGraph, new SuperTypeToken<>() {});\n        Set<DefaultEdge> remainingFAS = verifier.computeFeedbackArcSet();\n\n        if (remainingFAS.isEmpty()) {\n            System.out.println(\"✓ Verification successful: Graph is acyclic after FAS removal\");\n        } else {\n            System.out.println(\"✗ Verification failed: \" + remainingFAS.size() + \" cycles remain after FAS removal\");\n        }\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/arc/pageRank/PageRankFASTest.java",
    "content": "package org.hjug.feedback.arc.pageRank;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.*;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Nested;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Comprehensive unit tests for the PageRankFAS algorithm with custom LineDigraph\n */\nclass PageRankFASTest {\n\n    private PageRankFAS<String, DefaultEdge> pageRankFAS;\n\n    @Nested\n    @DisplayName(\"LineDigraph Implementation Tests\")\n    class LineDigraphTests {\n\n        @Test\n        @DisplayName(\"Test LineDigraph basic operations\")\n        void testLineDigraphBasicOperations() {\n            LineDigraph<String, DefaultEdge> lineDigraph = new LineDigraph<>();\n\n            // Test empty digraph\n            assertTrue(lineDigraph.isEmpty());\n            assertEquals(0, lineDigraph.vertexCount());\n            assertEquals(0, lineDigraph.edgeCount());\n\n            // Create test line vertices\n            DefaultEdge edge1 = new DefaultEdge();\n            DefaultEdge edge2 = new DefaultEdge();\n            LineVertex<String, DefaultEdge> lv1 = new LineVertex<>(\"A\", \"B\", edge1);\n            LineVertex<String, DefaultEdge> lv2 = new LineVertex<>(\"B\", \"C\", edge2);\n\n            // Test adding vertices\n            assertTrue(lineDigraph.addVertex(lv1));\n            assertFalse(lineDigraph.addVertex(lv1)); // Should not add duplicate\n            assertTrue(lineDigraph.addVertex(lv2));\n\n            assertEquals(2, lineDigraph.vertexCount());\n            assertTrue(lineDigraph.containsVertex(lv1));\n            assertTrue(lineDigraph.containsVertex(lv2));\n\n            // Test adding edges\n            assertTrue(lineDigraph.addEdge(lv1, lv2));\n            assertFalse(lineDigraph.addEdge(lv1, lv2)); // Should not add duplicate\n\n            assertEquals(1, lineDigraph.edgeCount());\n            assertTrue(lineDigraph.containsEdge(lv1, lv2));\n            assertFalse(lineDigraph.containsEdge(lv2, lv1));\n        }\n\n        @Test\n        @DisplayName(\"Test LineDigraph degree calculations\")\n        void testLineDigraphDegrees() {\n            LineDigraph<String, DefaultEdge> lineDigraph = new LineDigraph<>();\n\n            DefaultEdge e1 = new DefaultEdge();\n            DefaultEdge e2 = new DefaultEdge();\n            DefaultEdge e3 = new DefaultEdge();\n\n            LineVertex<String, DefaultEdge> lv1 = new LineVertex<>(\"A\", \"B\", e1);\n            LineVertex<String, DefaultEdge> lv2 = new LineVertex<>(\"B\", \"C\", e2);\n            LineVertex<String, DefaultEdge> lv3 = new LineVertex<>(\"C\", \"A\", e3);\n\n            lineDigraph.addVertex(lv1);\n            lineDigraph.addVertex(lv2);\n            lineDigraph.addVertex(lv3);\n\n            lineDigraph.addEdge(lv1, lv2);\n            lineDigraph.addEdge(lv2, lv3);\n            lineDigraph.addEdge(lv3, lv1);\n\n            // Test degrees\n            assertEquals(1, lineDigraph.getOutDegree(lv1));\n            assertEquals(1, lineDigraph.getInDegree(lv1));\n            assertEquals(2, lineDigraph.getTotalDegree(lv1));\n\n            // Test neighbors\n            assertEquals(Set.of(lv2), lineDigraph.getOutgoingNeighbors(lv1));\n            assertEquals(Set.of(lv3), lineDigraph.getIncomingNeighbors(lv1));\n            assertEquals(Set.of(lv2, lv3), lineDigraph.getAllNeighbors(lv1));\n        }\n\n        @Test\n        @DisplayName(\"Test LineDigraph sources and sinks\")\n        void testLineDigraphSourcesAndSinks() {\n            LineDigraph<String, DefaultEdge> lineDigraph = new LineDigraph<>();\n\n            DefaultEdge e1 = new DefaultEdge();\n            DefaultEdge e2 = new DefaultEdge();\n            DefaultEdge e3 = new DefaultEdge();\n\n            LineVertex<String, DefaultEdge> source = new LineVertex<>(\"A\", \"B\", e1);\n            LineVertex<String, DefaultEdge> middle = new LineVertex<>(\"B\", \"C\", e2);\n            LineVertex<String, DefaultEdge> sink = new LineVertex<>(\"C\", \"D\", e3);\n\n            lineDigraph.addVertex(source);\n            lineDigraph.addVertex(middle);\n            lineDigraph.addVertex(sink);\n\n            lineDigraph.addEdge(source, middle);\n            lineDigraph.addEdge(middle, sink);\n\n            // Test sources and sinks\n            assertEquals(Set.of(source), lineDigraph.getSources());\n            assertEquals(Set.of(sink), lineDigraph.getSinks());\n        }\n\n        @Test\n        @DisplayName(\"Test LineDigraph path finding\")\n        void testLineDigraphPathFinding() {\n            LineDigraph<String, DefaultEdge> lineDigraph = new LineDigraph<>();\n\n            DefaultEdge e1 = new DefaultEdge();\n            DefaultEdge e2 = new DefaultEdge();\n            DefaultEdge e3 = new DefaultEdge();\n\n            LineVertex<String, DefaultEdge> lv1 = new LineVertex<>(\"A\", \"B\", e1);\n            LineVertex<String, DefaultEdge> lv2 = new LineVertex<>(\"B\", \"C\", e2);\n            LineVertex<String, DefaultEdge> lv3 = new LineVertex<>(\"C\", \"D\", e3);\n\n            lineDigraph.addVertex(lv1);\n            lineDigraph.addVertex(lv2);\n            lineDigraph.addVertex(lv3);\n\n            lineDigraph.addEdge(lv1, lv2);\n            lineDigraph.addEdge(lv2, lv3);\n\n            // Test path existence\n            assertTrue(lineDigraph.hasPath(lv1, lv2));\n            assertTrue(lineDigraph.hasPath(lv1, lv3));\n            assertTrue(lineDigraph.hasPath(lv2, lv3));\n            assertFalse(lineDigraph.hasPath(lv3, lv1));\n\n            // Test reachable vertices\n            Set<LineVertex<String, DefaultEdge>> reachable = lineDigraph.getReachableVertices(lv1);\n            assertEquals(Set.of(lv1, lv2, lv3), reachable);\n        }\n\n        @Test\n        @DisplayName(\"Test LineDigraph topological sort\")\n        void testLineDigraphTopologicalSort() {\n            LineDigraph<String, DefaultEdge> lineDigraph = new LineDigraph<>();\n\n            DefaultEdge e1 = new DefaultEdge();\n            DefaultEdge e2 = new DefaultEdge();\n            DefaultEdge e3 = new DefaultEdge();\n\n            LineVertex<String, DefaultEdge> lv1 = new LineVertex<>(\"A\", \"B\", e1);\n            LineVertex<String, DefaultEdge> lv2 = new LineVertex<>(\"B\", \"C\", e2);\n            LineVertex<String, DefaultEdge> lv3 = new LineVertex<>(\"C\", \"D\", e3);\n\n            lineDigraph.addVertex(lv1);\n            lineDigraph.addVertex(lv2);\n            lineDigraph.addVertex(lv3);\n\n            lineDigraph.addEdge(lv1, lv2);\n            lineDigraph.addEdge(lv2, lv3);\n\n            // Test topological sort on acyclic graph\n            List<LineVertex<String, DefaultEdge>> sorted = lineDigraph.topologicalSort();\n            assertEquals(3, sorted.size());\n            assertEquals(lv1, sorted.get(0));\n            assertEquals(lv2, sorted.get(1));\n            assertEquals(lv3, sorted.get(2));\n\n            // Add cycle and test\n            lineDigraph.addEdge(lv3, lv1);\n            List<LineVertex<String, DefaultEdge>> cyclicSort = lineDigraph.topologicalSort();\n            assertTrue(cyclicSort.isEmpty()); // Should return empty for cyclic graphs\n        }\n\n        @Test\n        @DisplayName(\"Test LineDigraph consistency validation\")\n        void testLineDigraphConsistency() {\n            LineDigraph<String, DefaultEdge> lineDigraph = new LineDigraph<>();\n\n            DefaultEdge e1 = new DefaultEdge();\n            DefaultEdge e2 = new DefaultEdge();\n\n            LineVertex<String, DefaultEdge> lv1 = new LineVertex<>(\"A\", \"B\", e1);\n            LineVertex<String, DefaultEdge> lv2 = new LineVertex<>(\"B\", \"C\", e2);\n\n            lineDigraph.addVertex(lv1);\n            lineDigraph.addVertex(lv2);\n            lineDigraph.addEdge(lv1, lv2);\n\n            // Should be consistent\n            assertTrue(lineDigraph.validateConsistency());\n\n            // Test copy operation\n            LineDigraph<String, DefaultEdge> copy = lineDigraph.copy();\n            assertEquals(lineDigraph.vertexCount(), copy.vertexCount());\n            assertEquals(lineDigraph.edgeCount(), copy.edgeCount());\n            assertTrue(copy.validateConsistency());\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Updated PageRankFAS Algorithm Tests\")\n    class UpdatedAlgorithmTests {\n\n        @Test\n        @DisplayName(\"Test updated algorithm on simple cycle\")\n        void testUpdatedAlgorithmSimpleCycle() {\n            Graph<String, DefaultEdge> graph = createSimpleCycle();\n            pageRankFAS = new PageRankFAS<>(graph, new SuperTypeToken<>() {});\n\n            Set<DefaultEdge> fas = pageRankFAS.computeFeedbackArcSet();\n\n            assertEquals(1, fas.size(), \"FAS should contain exactly one edge for simple cycle\");\n\n            // Verify that removing the FAS makes the graph acyclic\n            fas.forEach(graph::removeEdge);\n            PageRankFAS<String, DefaultEdge> verifier = new PageRankFAS<>(graph, new SuperTypeToken<>() {});\n            assertTrue(verifier.computeFeedbackArcSet().isEmpty(), \"Graph should be acyclic after removing FAS\");\n        }\n\n        @Test\n        @DisplayName(\"Test updated algorithm execution statistics\")\n        void testExecutionStatistics() {\n            Graph<String, DefaultEdge> graph = createComplexGraph();\n            pageRankFAS = new PageRankFAS<>(graph, new SuperTypeToken<>() {});\n\n            Map<String, Object> stats = pageRankFAS.getExecutionStatistics(graph);\n\n            assertNotNull(stats);\n            assertTrue(stats.containsKey(\"originalVertices\"));\n            assertTrue(stats.containsKey(\"originalEdges\"));\n            assertTrue(stats.containsKey(\"pageRankIterations\"));\n            assertTrue(stats.containsKey(\"sccCount\"));\n            assertTrue(stats.containsKey(\"trivialSCCs\"));\n            assertTrue(stats.containsKey(\"nonTrivialSCCs\"));\n            assertTrue(stats.containsKey(\"largestSCCSize\"));\n\n            assertEquals(graph.vertexSet().size(), stats.get(\"originalVertices\"));\n            assertEquals(graph.edgeSet().size(), stats.get(\"originalEdges\"));\n        }\n\n        @Test\n        @DisplayName(\"Test updated algorithm with multiple SCCs\")\n        void testMultipleSCCs() {\n            Graph<String, DefaultEdge> graph = createMultipleSCCGraph();\n            pageRankFAS = new PageRankFAS<>(graph, new SuperTypeToken<>() {});\n\n            Set<DefaultEdge> fas = pageRankFAS.computeFeedbackArcSet();\n\n            // Verify that the result breaks all cycles\n            fas.forEach(graph::removeEdge);\n            PageRankFAS<String, DefaultEdge> verifier = new PageRankFAS<>(graph, new SuperTypeToken<>() {});\n            assertTrue(verifier.computeFeedbackArcSet().isEmpty(), \"Graph should be acyclic after removing FAS\");\n\n            // Check execution statistics\n            Map<String, Object> stats = pageRankFAS.getExecutionStatistics(createMultipleSCCGraph());\n            assertTrue((Integer) stats.get(\"nonTrivialSCCs\") >= 2, \"Should have multiple non-trivial SCCs\");\n        }\n\n        @Test\n        @DisplayName(\"Test performance comparison with different PageRank iterations\")\n        void testPerformanceWithDifferentIterations() {\n            Graph<String, DefaultEdge> graph = createComplexGraph();\n\n            int[] iterations = {1, 3, 5, 10};\n            Map<Integer, Integer> fasSize = new HashMap<>();\n            Map<Integer, Long> executionTime = new HashMap<>();\n\n            for (int iter : iterations) {\n                Graph<String, DefaultEdge> testGraph = copyGraph(graph);\n                PageRankFAS<String, DefaultEdge> algorithm =\n                        new PageRankFAS<>(testGraph, iter, new SuperTypeToken<>() {});\n\n                long startTime = System.currentTimeMillis();\n                Set<DefaultEdge> fas = algorithm.computeFeedbackArcSet();\n                long endTime = System.currentTimeMillis();\n\n                fasSize.put(iter, fas.size());\n                executionTime.put(iter, endTime - startTime);\n\n                // Verify correctness\n                fas.forEach(testGraph::removeEdge);\n                PageRankFAS<String, DefaultEdge> verifier = new PageRankFAS<>(testGraph, new SuperTypeToken<>() {});\n                assertTrue(\n                        verifier.computeFeedbackArcSet().isEmpty(),\n                        \"Graph should be acyclic after removing FAS (iter=\" + iter + \")\");\n            }\n\n            // Log results for analysis\n            System.out.println(\"Performance analysis:\");\n            for (int iter : iterations) {\n                System.out.printf(\n                        \"Iterations: %d, FAS size: %d, Time: %dms%n\", iter, fasSize.get(iter), executionTime.get(iter));\n            }\n        }\n    }\n\n    // Helper methods for creating test graphs\n    private Graph<String, DefaultEdge> createSimpleCycle() {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n        graph.addVertex(\"A\");\n        graph.addVertex(\"B\");\n        graph.addVertex(\"C\");\n\n        graph.addEdge(\"A\", \"B\");\n        graph.addEdge(\"B\", \"C\");\n        graph.addEdge(\"C\", \"A\");\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createComplexGraph() {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Create vertices\n        for (int i = 0; i < 8; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        // Create multiple cycles\n        graph.addEdge(\"V0\", \"V1\");\n        graph.addEdge(\"V1\", \"V2\");\n        graph.addEdge(\"V2\", \"V0\"); // Triangle cycle\n\n        graph.addEdge(\"V3\", \"V4\");\n        graph.addEdge(\"V4\", \"V5\");\n        graph.addEdge(\"V5\", \"V6\");\n        graph.addEdge(\"V6\", \"V3\"); // Square cycle\n\n        // Overlapping cycle\n        graph.addEdge(\"V2\", \"V3\");\n        graph.addEdge(\"V5\", \"V7\");\n        graph.addEdge(\"V7\", \"V1\"); // Creates larger cycle\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createMultipleSCCGraph() {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // SCC 1: A <-> B\n        graph.addVertex(\"A\");\n        graph.addVertex(\"B\");\n        graph.addEdge(\"A\", \"B\");\n        graph.addEdge(\"B\", \"A\");\n\n        // SCC 2: C <-> D <-> E\n        graph.addVertex(\"C\");\n        graph.addVertex(\"D\");\n        graph.addVertex(\"E\");\n        graph.addEdge(\"C\", \"D\");\n        graph.addEdge(\"D\", \"E\");\n        graph.addEdge(\"E\", \"C\");\n\n        // SCC 3: F -> G -> H -> F\n        graph.addVertex(\"F\");\n        graph.addVertex(\"G\");\n        graph.addVertex(\"H\");\n        graph.addEdge(\"F\", \"G\");\n        graph.addEdge(\"G\", \"H\");\n        graph.addEdge(\"H\", \"F\");\n\n        // Connections between SCCs (acyclic)\n        graph.addEdge(\"B\", \"C\");\n        graph.addEdge(\"E\", \"F\");\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> copyGraph(Graph<String, DefaultEdge> original) {\n        Graph<String, DefaultEdge> copy = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        original.vertexSet().forEach(copy::addVertex);\n\n        // Add edges\n        original.edgeSet().forEach(edge -> {\n            String source = original.getEdgeSource(edge);\n            String target = original.getEdgeTarget(edge);\n            copy.addEdge(source, target);\n        });\n\n        return copy;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/vertex/approximate/FeedbackVertexSetBenchmarkTest.java",
    "content": "package org.hjug.feedback.vertex.approximate;\n\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.IntStream;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Performance benchmark tests[8]\n */\nclass FeedbackVertexSetBenchmarkTest {\n\n    @Test\n    @DisplayName(\"Benchmark: Various graph sizes and densities\")\n    void benchmarkGraphSizes() {\n        int[] sizes = {20, 50, 100, 200};\n        double[] densities = {0.1, 0.3, 0.5};\n\n        System.out.println(\"=== Feedback Vertex Set Benchmark ===\");\n        System.out.printf(\"%-10s %-15s %-15s %-15s %-15s%n\", \"Size\", \"Density\", \"Vertices\", \"Edges\", \"Time (ms)\");\n\n        for (int size : sizes) {\n            for (double density : densities) {\n                Graph<String, DefaultEdge> graph = createRandomGraph(size, density);\n\n                long startTime = System.currentTimeMillis();\n                FeedbackVertexSetSolver<String, DefaultEdge> solver =\n                        new FeedbackVertexSetSolver<>(graph, null, null, 0.1);\n                FeedbackVertexSetResult<String> result = solver.solve();\n                long endTime = System.currentTimeMillis();\n\n                System.out.printf(\n                        \"%-10d %-15.1f %-15d %-15d %-15d%n\",\n                        size, density, graph.vertexSet().size(), graph.edgeSet().size(), endTime - startTime);\n            }\n        }\n    }\n\n    private Graph<String, DefaultEdge> createRandomGraph(int size, double density) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        IntStream.range(0, size).forEach(i -> graph.addVertex(\"V\" + i));\n\n        List<String> vertices = new ArrayList<>(graph.vertexSet());\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        int maxEdges = size * (size - 1);\n        int targetEdges = (int) (maxEdges * density);\n\n        int addedEdges = 0;\n        while (addedEdges < targetEdges) {\n            String source = vertices.get(random.nextInt(vertices.size()));\n            String target = vertices.get(random.nextInt(vertices.size()));\n\n            if (!source.equals(target) && !graph.containsEdge(source, target)) {\n                graph.addEdge(source, target);\n                addedEdges++;\n            }\n        }\n\n        return graph;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/vertex/approximate/FeedbackVertexSetExample.java",
    "content": "package org.hjug.feedback.vertex.approximate;\n\nimport java.util.Map;\nimport java.util.Set;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\n\npublic class FeedbackVertexSetExample {\n    public static void main(String[] args) {\n        // Create a directed graph with cycles\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        graph.addVertex(\"A\");\n        graph.addVertex(\"B\");\n        graph.addVertex(\"C\");\n        graph.addVertex(\"D\");\n\n        // Add edges creating cycles\n        graph.addEdge(\"A\", \"B\");\n        graph.addEdge(\"B\", \"C\");\n        graph.addEdge(\"C\", \"A\"); // Creates cycle A->B->C->A\n        graph.addEdge(\"C\", \"D\");\n        graph.addEdge(\"D\", \"A\"); // Creates cycle A->B->C->D->A\n\n        // Define vertex weights (optional)\n        Map<String, Double> weights = Map.of(\"A\", 1.0, \"B\", 2.0, \"C\", 1.5, \"D\", 1.0);\n\n        // Define special vertices (optional - all vertices by default)\n        Set<String> specialVertices = Set.of(\"A\", \"B\", \"C\", \"D\");\n\n        // Solve the FVS problem\n        FeedbackVertexSetSolver<String, DefaultEdge> solver =\n                new FeedbackVertexSetSolver<>(graph, specialVertices, weights, 0.1);\n        FeedbackVertexSetResult<String> result = solver.solve();\n\n        System.out.println(\"Feedback vertex set: \" + result.getFeedbackVertices());\n        System.out.println(\"Solution size: \" + result.size());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/vertex/approximate/FeedbackVertexSetSolverTest.java",
    "content": "package org.hjug.feedback.vertex.approximate;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.IntStream;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.parallel.Execution;\nimport org.junit.jupiter.api.parallel.ExecutionMode;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\n/**\n * Comprehensive unit tests for the FeedbackVertexSetSolver[6]\n */\n@Execution(ExecutionMode.CONCURRENT)\nclass FeedbackVertexSetSolverTest {\n\n    private Graph<String, DefaultEdge> graph;\n    private FeedbackVertexSetSolver<String, DefaultEdge> solver;\n\n    @BeforeEach\n    void setUp() {\n        graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n    }\n\n    @Nested\n    @DisplayName(\"Basic Algorithm Tests\")\n    class BasicAlgorithmTests {\n\n        @Test\n        @DisplayName(\"Should handle empty graph\")\n        void testEmptyGraph() {\n            solver = new FeedbackVertexSetSolver<>(graph, null, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            assertTrue(result.getFeedbackVertices().isEmpty());\n            assertEquals(0, result.size());\n        }\n\n        @Test\n        @DisplayName(\"Should handle single vertex\")\n        void testSingleVertex() {\n            graph.addVertex(\"A\");\n            solver = new FeedbackVertexSetSolver<>(graph, null, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            assertEquals(0, result.size());\n        }\n\n        @Test\n        @DisplayName(\"Should handle acyclic graph\")\n        void testAcyclicGraph() {\n            // Create a simple DAG: A -> B -> C[7]\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n\n            solver = new FeedbackVertexSetSolver<>(graph, null, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            assertEquals(0, result.size());\n        }\n\n        @Test\n        @DisplayName(\"Should handle simple cycle\")\n        void testSimpleCycle() {\n            // Create a simple cycle: A -> B -> C -> A[7]\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n\n            solver = new FeedbackVertexSetSolver<>(graph, null, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            // Should break the cycle with at least one vertex\n            assertTrue(result.size() >= 1);\n            assertFalse(isGraphIsAcyclicAfterRemoval(result));\n        }\n\n        @Test\n        @DisplayName(\"Should handle self-loop\")\n        void testSelfLoop() {\n            graph.addVertex(\"A\");\n            graph.addEdge(\"A\", \"A\");\n\n            Set<String> specialVertices = Set.of(\"A\");\n            solver = new FeedbackVertexSetSolver<>(graph, specialVertices, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            assertEquals(1, result.size());\n            assertTrue(result.getFeedbackVertices().contains(\"A\"));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Complex Graph Tests\")\n    class ComplexGraphTests {\n\n        @Test\n        @DisplayName(\"Should handle multiple cycles\")\n        void testMultipleCycles() {\n            // Create graph with multiple overlapping cycles[5]\n            String[] vertices = {\"A\", \"B\", \"C\", \"D\", \"E\"};\n            for (String v : vertices) {\n                graph.addVertex(v);\n            }\n\n            // Create cycles: A->B->C->A and C->D->E->C\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n            graph.addEdge(\"C\", \"D\");\n            graph.addEdge(\"D\", \"E\");\n            graph.addEdge(\"E\", \"C\");\n\n            solver = new FeedbackVertexSetSolver<>(graph, null, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            assertTrue(result.size() >= 1);\n            assertFalse(isGraphIsAcyclicAfterRemoval(result));\n        }\n\n        @Test\n        @DisplayName(\"Should handle disconnected components\")\n        void testDisconnectedComponents() {\n            // Component 1: A -> B -> A\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"A\");\n\n            // Component 2: C -> D (acyclic)\n            graph.addVertex(\"C\");\n            graph.addVertex(\"D\");\n            graph.addEdge(\"C\", \"D\");\n\n            // Component 3: E (isolated)\n            graph.addVertex(\"E\");\n\n            solver = new FeedbackVertexSetSolver<>(graph, null, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            assertTrue(result.size() >= 1);\n            assertFalse(isGraphIsAcyclicAfterRemoval(result));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Performance Tests\")\n    class PerformanceTests {\n\n        @ParameterizedTest\n        @ValueSource(ints = {10, 25, 50})\n        @DisplayName(\"Should handle random graphs efficiently\")\n        @Disabled(\"Not consistent\")\n        void testRandomGraphPerformance(int size) {\n            createRandomGraph(size, size * 2);\n\n            long startTime = System.currentTimeMillis();\n            solver = new FeedbackVertexSetSolver<>(graph, null, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n            long endTime = System.currentTimeMillis();\n\n            // Performance should be reasonable[8]\n            assertTrue(endTime - startTime < 20000, \"Algorithm took too long: \" + (endTime - startTime) + \"ms\");\n\n            if (hasCycles(graph)) {\n                assertFalse(isGraphIsAcyclicAfterRemoval(result));\n            }\n        }\n\n        @Test\n        @DisplayName(\"Should handle weighted vertices\")\n        @Disabled(\"Not planning to use weighted vertices\")\n        void testWeightedVertices() {\n            // Create a cycle with different vertex weights\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n\n            Map<String, Double> weights = Map.of(\"A\", 1.0, \"B\", 10.0, \"C\", 1.0);\n\n            solver = new FeedbackVertexSetSolver<>(graph, null, weights, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            assertTrue(result.size() >= 1);\n            // Should prefer removing lower weight vertices\n            System.out.println(\"Feedback vertices: \" + result.getFeedbackVertices());\n            assertFalse(result.getFeedbackVertices().contains(\"B\"));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Correctness Tests\")\n    class CorrectnessTests {\n\n        @Test\n        @DisplayName(\"Should maintain approximation guarantees\")\n        @Disabled(\"Not consistent\")\n        void testApproximationBounds() {\n            createRandomGraph(20, 40);\n\n            solver = new FeedbackVertexSetSolver<>(graph, null, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            // The solution should be bounded by the theoretical guarantees[1]\n            int n = graph.vertexSet().size();\n            assertTrue(result.size() <= n, \"Solution size should be at most n\");\n\n            if (hasCycles(graph)) {\n                assertFalse(isGraphIsAcyclicAfterRemoval(result));\n            }\n        }\n\n        @Test\n        @DisplayName(\"Should handle special vertex constraints\")\n        void testSpecialVertexConstraints() {\n            // Create cycle where only some vertices are \"special\"\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addVertex(\"D\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"D\");\n            graph.addEdge(\"D\", \"A\");\n\n            Set<String> specialVertices = Set.of(\"A\", \"C\"); // Only A and C are special\n            solver = new FeedbackVertexSetSolver<>(graph, specialVertices, null, 0.1);\n            FeedbackVertexSetResult<String> result = solver.solve();\n\n            // Should only consider cycles involving special vertices\n            assertTrue(result.size() >= 1);\n        }\n    }\n\n    // Helper methods\n\n    private void createRandomGraph(int vertexCount, int edgeCount) {\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        // Add vertices[10]\n        IntStream.range(0, vertexCount).forEach(i -> graph.addVertex(\"V\" + i));\n        List<String> vertices = new ArrayList<>(graph.vertexSet());\n\n        // Add random edges\n        int addedEdges = 0;\n        while (addedEdges < edgeCount && addedEdges < vertexCount * (vertexCount - 1)) {\n            String source = vertices.get(random.nextInt(vertices.size()));\n            String target = vertices.get(random.nextInt(vertices.size()));\n\n            if (!source.equals(target) && !graph.containsEdge(source, target)) {\n                graph.addEdge(source, target);\n                addedEdges++;\n            }\n        }\n    }\n\n    private boolean hasCycles(Graph<String, DefaultEdge> graph) {\n        CycleDetector<String, DefaultEdge> cycleDetector = new CycleDetector<>(graph);\n        return cycleDetector.detectCycles();\n    }\n\n    private boolean isGraphIsAcyclicAfterRemoval(FeedbackVertexSetResult<String> result) {\n        Graph<String, DefaultEdge> testGraph = createGraphWithoutFeedbackVertices(result);\n\n        // Verify the resulting graph is acyclic[6]\n        CycleDetector<String, DefaultEdge> cycleDetector = new CycleDetector<>(testGraph);\n        System.out.println(cycleDetector.findCycles());\n        return cycleDetector.detectCycles();\n        //        assertFalse(hasCycles, \"Graph should be acyclic after removing feedback vertices\");\n    }\n\n    private Graph<String, DefaultEdge> createGraphWithoutFeedbackVertices(FeedbackVertexSetResult<String> result) {\n        // Create a copy of the graph without feedback vertices[6]\n        Graph<String, DefaultEdge> testGraph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices except feedback vertices\n        graph.vertexSet().stream()\n                .filter(v -> !result.getFeedbackVertices().contains(v))\n                .forEach(testGraph::addVertex);\n\n        // Add edges between remaining vertices\n        for (DefaultEdge edge : graph.edgeSet()) {\n            String source = graph.getEdgeSource(edge);\n            String target = graph.getEdgeTarget(edge);\n\n            if (testGraph.containsVertex(source) && testGraph.containsVertex(target)) {\n                testGraph.addEdge(source, target);\n            }\n        }\n        return testGraph;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/vertex/kernelized/DirectedFeedbackVertexSetBenchmarkTest.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.IntStream;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.DisplayName;\nimport org.junit.jupiter.api.Test;\n\n/**\n * Performance benchmark tests for the kernelization algorithm[1]\n */\nclass DirectedFeedbackVertexSetBenchmarkTest {\n\n    @Test\n    @DisplayName(\"Benchmark: Various graph sizes and treewidth parameters\")\n    void benchmarkGraphSizes() {\n        int[] sizes = {20, 50, 100};\n        int[] etaValues = {1, 2, 3};\n        double[] densities = {0.1, 0.3, 0.5};\n\n        System.out.println(\"=== Directed Feedback Vertex Set Benchmark ===\");\n        System.out.printf(\n                \"%-10s %-10s %-15s %-15s %-15s %-15s%n\", \"Size\", \"Eta\", \"Density\", \"Vertices\", \"Edges\", \"Time (ms)\");\n\n        for (int size : sizes) {\n            for (int eta : etaValues) {\n                for (double density : densities) {\n                    Graph<String, DefaultEdge> graph = createRandomGraph(size, density);\n\n                    long startTime = System.currentTimeMillis();\n                    DirectedFeedbackVertexSetSolver<String, DefaultEdge> solver =\n                            new DirectedFeedbackVertexSetSolver<>(graph, null, null, eta, new SuperTypeToken<>() {});\n                    DirectedFeedbackVertexSetResult<String> result = solver.solve(size / 4);\n                    long endTime = System.currentTimeMillis();\n\n                    System.out.printf(\n                            \"%-10d %-10d %-15.1f %-15d %-15d %-15d%n\",\n                            size,\n                            eta,\n                            density,\n                            graph.vertexSet().size(),\n                            graph.edgeSet().size(),\n                            endTime - startTime);\n                }\n            }\n        }\n    }\n\n    private Graph<String, DefaultEdge> createRandomGraph(int size, double density) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        IntStream.range(0, size).forEach(i -> graph.addVertex(\"V\" + i));\n\n        List<String> vertices = new ArrayList<>(graph.vertexSet());\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        int maxEdges = size * (size - 1);\n        int targetEdges = (int) (maxEdges * density);\n\n        int addedEdges = 0;\n        while (addedEdges < targetEdges) {\n            String source = vertices.get(random.nextInt(vertices.size()));\n            String target = vertices.get(random.nextInt(vertices.size()));\n\n            if (!source.equals(target) && !graph.containsEdge(source, target)) {\n                graph.addEdge(source, target);\n                addedEdges++;\n            }\n        }\n\n        return graph;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/vertex/kernelized/DirectedFeedbackVertexSetExample.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport java.util.Map;\nimport java.util.Set;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\n\npublic class DirectedFeedbackVertexSetExample {\n    public static void main(String[] args) {\n        // Create a directed graph with cycles\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        graph.addVertex(\"A\");\n        graph.addVertex(\"B\");\n        graph.addVertex(\"C\");\n        graph.addVertex(\"D\");\n\n        // Add edges creating cycles\n        graph.addEdge(\"A\", \"B\");\n        graph.addEdge(\"B\", \"C\");\n        graph.addEdge(\"C\", \"A\"); // Creates cycle A->B->C->A\n        graph.addEdge(\"C\", \"D\");\n        graph.addEdge(\"D\", \"A\"); // Creates cycle A->B->C->D->A\n\n        // Define treewidth modulator (optional)\n        Set<String> modulator = Set.of(\"A\", \"C\");\n\n        // Define vertex weights (optional)\n        Map<String, Double> weights = Map.of(\"A\", 1.0, \"B\", 2.0, \"C\", 1.5, \"D\", 1.0);\n\n        // Solve the DFVS problem with treewidth parameter η=2\n        DirectedFeedbackVertexSetSolver<String, DefaultEdge> solver =\n                new DirectedFeedbackVertexSetSolver<>(graph, modulator, weights, 2, new SuperTypeToken<>() {});\n        DirectedFeedbackVertexSetResult<String> result = solver.solve(3);\n\n        System.out.println(\"Feedback vertex set: \" + result.getFeedbackVertices());\n        System.out.println(\"Solution size: \" + result.size());\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/vertex/kernelized/DirectedFeedbackVertexSetSolverTest.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.IntStream;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.alg.cycle.CycleDetector;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.parallel.Execution;\nimport org.junit.jupiter.api.parallel.ExecutionMode;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\n/**\n * Comprehensive unit tests for the DirectedFeedbackVertexSetSolver[1]\n */\n@Execution(ExecutionMode.CONCURRENT)\nclass DirectedFeedbackVertexSetSolverTest {\n\n    private Graph<String, DefaultEdge> graph;\n    private DirectedFeedbackVertexSetSolver<String, DefaultEdge> solver;\n\n    @BeforeEach\n    void setUp() {\n        graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n    }\n\n    @Nested\n    @DisplayName(\"Basic Algorithm Tests\")\n    class BasicAlgorithmTests {\n\n        @Test\n        @DisplayName(\"Should handle empty graph\")\n        void testEmptyGraph() {\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, null, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(1);\n\n            assertTrue(result.getFeedbackVertices().isEmpty());\n            assertEquals(0, result.size());\n        }\n\n        @Test\n        @DisplayName(\"Should handle single vertex\")\n        void testSingleVertex() {\n            graph.addVertex(\"A\");\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, null, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(1);\n\n            assertEquals(0, result.size());\n        }\n\n        @Test\n        @DisplayName(\"Should handle acyclic graph\")\n        void testAcyclicGraph() {\n            // Create a simple DAG: A -> B -> C[17]\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, null, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(2);\n\n            assertEquals(0, result.size());\n        }\n\n        @Test\n        @DisplayName(\"Should handle simple cycle\")\n        void testSimpleCycle() {\n            // Create a simple cycle: A -> B -> C -> A\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, null, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(2);\n\n            // Should break the cycle with at least one vertex\n            assertTrue(result.size() >= 1);\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n\n        @Test\n        @DisplayName(\"Should handle self-loop\")\n        void testSelfLoop() {\n            graph.addVertex(\"A\");\n            graph.addEdge(\"A\", \"A\");\n\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, null, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(1);\n\n            assertEquals(1, result.size());\n            assertTrue(result.getFeedbackVertices().contains(\"A\"));\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Complex Graph Tests\")\n    class ComplexGraphTests {\n\n        @Test\n        @DisplayName(\"Should handle multiple cycles\")\n        void testMultipleCycles() {\n            // Create graph with multiple overlapping cycles\n            String[] vertices = {\"A\", \"B\", \"C\", \"D\", \"E\"};\n            for (String v : vertices) {\n                graph.addVertex(v);\n            }\n\n            // Create cycles: A->B->C->A and C->D->E->C\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n            graph.addEdge(\"C\", \"D\");\n            graph.addEdge(\"D\", \"E\");\n            graph.addEdge(\"E\", \"C\");\n\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, null, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(3);\n\n            assertTrue(result.size() >= 1);\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n\n        @Test\n        @DisplayName(\"Should handle treewidth modulator\")\n        void testTreewidthModulator() {\n            // Create a graph with a known modulator\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addVertex(\"D\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n            graph.addEdge(\"A\", \"D\");\n\n            Set<String> modulator = Set.of(\"A\"); // A is the modulator\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, modulator, null, 1, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(2); // there are 2 SCCs\n\n            // removing A breaks the graph into 2 distinct trees: B->C, D\n            // no results means there are no feedback vertices to remove\n            assertTrue(result.size() == 0);\n        }\n\n        @Test\n        @DisplayName(\"Should handle weighted vertices\")\n        void testWeightedVertices() {\n            // Create a cycle with different vertex weights\n            graph.addVertex(\"A\");\n            graph.addVertex(\"B\");\n            graph.addVertex(\"C\");\n            graph.addEdge(\"A\", \"B\");\n            graph.addEdge(\"B\", \"C\");\n            graph.addEdge(\"C\", \"A\");\n\n            Map<String, Double> weights = Map.of(\"A\", 1.0, \"B\", 10.0, \"C\", 1.0);\n\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, weights, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(2);\n\n            assertTrue(result.size() >= 1);\n            // Should prefer removing lower weight vertices\n            if (result.size() == 1) {\n                assertFalse(result.getFeedbackVertices().contains(\"B\"));\n            }\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Performance Tests\")\n    @Disabled(\"Not consistent\")\n    class PerformanceTests {\n\n        @ParameterizedTest\n        @ValueSource(ints = {10, 25, 50})\n        @DisplayName(\"Should handle random graphs efficiently\")\n        void testRandomGraphPerformance(int size) {\n            createRandomGraph(size, size * 2);\n\n            long startTime = System.currentTimeMillis();\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, null, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(size / 3);\n            long endTime = System.currentTimeMillis();\n\n            // Performance should be reasonable[1]\n            assertTrue(endTime - startTime < 20000, \"Algorithm took too long: \" + (endTime - startTime) + \"ms\");\n\n            if (hasCycles()) {\n                assertGraphIsAcyclicAfterRemoval(result);\n            }\n        }\n\n        @Test\n        @DisplayName(\"Should utilize parallel processing effectively\")\n        void testParallelProcessing() {\n            createRandomGraph(30, 60);\n\n            long startTime = System.currentTimeMillis();\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, null, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(10);\n            long endTime = System.currentTimeMillis();\n\n            assertTrue(endTime - startTime < 15000);\n            if (hasCycles()) {\n                assertGraphIsAcyclicAfterRemoval(result);\n            }\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Kernelization Tests\")\n    class KernelizationTests {\n\n        @Test\n        @DisplayName(\"Should maintain kernelization properties\")\n        @Disabled(\"Not consistent\")\n        void testKernelizationProperties() {\n            createRandomGraph(20, 40);\n\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, null, null, 2, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(5);\n\n            // Solution should be bounded by the kernelization guarantees[1]\n            int n = graph.vertexSet().size();\n            assertTrue(result.size() <= n, \"Solution size should be at most n\");\n\n            if (hasCycles()) {\n                assertGraphIsAcyclicAfterRemoval(result);\n            }\n        }\n\n        @Test\n        @DisplayName(\"Should handle zone decomposition correctly\")\n        void testZoneDecomposition() {\n            // Create a graph that will trigger zone decomposition\n            graph.addVertex(\"M1\"); // Modulator vertex\n            graph.addVertex(\"Z1\"); // Zone vertex 1\n            graph.addVertex(\"Z2\"); // Zone vertex 2\n            graph.addVertex(\"Z3\"); // Zone vertex 3\n\n            graph.addEdge(\"M1\", \"Z1\");\n            graph.addEdge(\"Z1\", \"Z2\");\n            graph.addEdge(\"Z2\", \"Z3\");\n            graph.addEdge(\"Z3\", \"Z1\"); // Creates cycle in zone\n\n            Set<String> modulator = Set.of(\"M1\");\n            solver = new DirectedFeedbackVertexSetSolver<>(graph, modulator, null, 1, new SuperTypeToken<>() {});\n            DirectedFeedbackVertexSetResult<String> result = solver.solve(2);\n\n            assertTrue(result.size() >= 1);\n            assertGraphIsAcyclicAfterRemoval(result);\n        }\n    }\n\n    // Helper methods\n\n    private void createRandomGraph(int vertexCount, int edgeCount) {\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        // Add vertices [18]\n        IntStream.range(0, vertexCount).forEach(i -> graph.addVertex(\"V\" + i));\n\n        List<String> vertices = new ArrayList<>(graph.vertexSet());\n\n        // Add random edges\n        int addedEdges = 0;\n        while (addedEdges < edgeCount && addedEdges < vertexCount * (vertexCount - 1)) {\n            String source = vertices.get(random.nextInt(vertices.size()));\n            String target = vertices.get(random.nextInt(vertices.size()));\n\n            if (!source.equals(target) && !graph.containsEdge(source, target)) {\n                graph.addEdge(source, target);\n                addedEdges++;\n            }\n        }\n    }\n\n    private boolean hasCycles() {\n        CycleDetector<String, DefaultEdge> cycleDetector = new CycleDetector<>(graph);\n        return cycleDetector.detectCycles();\n    }\n\n    private void assertGraphIsAcyclicAfterRemoval(DirectedFeedbackVertexSetResult<String> result) {\n        // Create a copy of the graph without feedback vertices[17]\n        Graph<String, DefaultEdge> testGraph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices except feedback vertices\n        graph.vertexSet().stream()\n                .filter(v -> !result.getFeedbackVertices().contains(v))\n                .forEach(testGraph::addVertex);\n\n        // Add edges between remaining vertices\n        graph.edgeSet().forEach(edge -> {\n            String source = graph.getEdgeSource(edge);\n            String target = graph.getEdgeTarget(edge);\n\n            if (testGraph.containsVertex(source) && testGraph.containsVertex(target)) {\n                testGraph.addEdge(source, target);\n            }\n        });\n\n        // Verify the resulting graph is acyclic[17]\n        CycleDetector<String, DefaultEdge> cycleDetector = new CycleDetector<>(testGraph);\n        assertFalse(cycleDetector.detectCycles(), \"Graph should be acyclic after removing feedback vertices\");\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/vertex/kernelized/ModulatorComputerTest.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.IntStream;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.parallel.Execution;\nimport org.junit.jupiter.api.parallel.ExecutionMode;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\n@Execution(ExecutionMode.CONCURRENT)\nclass ModulatorComputerTest {\n\n    private ModulatorComputer<String, DefaultEdge> modulatorComputer;\n    private EnhancedParameterComputer<String, DefaultEdge> parameterComputer;\n    private SuperTypeToken<DefaultEdge> token;\n\n    @BeforeEach\n    void setUp() {\n        token = new SuperTypeToken<>() {};\n        modulatorComputer = new ModulatorComputer<>(token);\n        parameterComputer = new EnhancedParameterComputer<>(token);\n    }\n\n    @AfterEach\n    void tearDown() {\n        modulatorComputer.shutdown();\n        parameterComputer.shutdown();\n    }\n\n    @Nested\n    @DisplayName(\"Modulator Computation Tests\")\n    class ModulatorComputationTests {\n\n        @Test\n        @DisplayName(\"Should compute empty modulator for tree graph\")\n        void testTreeGraphModulator() {\n            Graph<String, DefaultEdge> tree = createTreeGraph(10);\n            ModulatorComputer.ModulatorResult<String> result = modulatorComputer.computeModulator(tree, 1, 5);\n\n            assertTrue(result.getResultingTreewidth() <= 1);\n            assertTrue(result.getSize() <= 2); // Trees have treewidth 1\n        }\n\n        @Test\n        @DisplayName(\"Should compute valid modulator for cycle graph\")\n        void testCycleGraphModulator() {\n            Graph<String, DefaultEdge> cycle = createCycleGraph(6);\n            ModulatorComputer.ModulatorResult<String> result = modulatorComputer.computeModulator(cycle, 1, 3);\n            /*A tree has treewidth = 1.\n            A cycle has treewidth = 2.\n            A clique of size n has treewidth = n-1\n            The more “grid-like” or “dense” the graph, the higher its treewidth.*/\n            assertTrue(result.getResultingTreewidth() <= 2); // this is a cycle\n            assertTrue(result.getSize() >= 1); // Need to break cycle\n            assertFalse(result.getModulator().isEmpty());\n        }\n\n        @Test\n        @DisplayName(\"Should compute modulator for complete graph\")\n        void testCompleteGraphModulator() {\n            Graph<String, DefaultEdge> complete = createCompleteGraph(5);\n            ModulatorComputer.ModulatorResult<String> result = modulatorComputer.computeModulator(complete, 2, 4);\n\n            assertTrue(result.getResultingTreewidth() <= 2);\n            assertTrue(result.getSize() >= 2); // Complete graphs have high treewidth\n        }\n\n        @Test\n        @DisplayName(\"Should respect modulator size limit\")\n        void testModulatorSizeLimit() {\n            Graph<String, DefaultEdge> complete = createCompleteGraph(8);\n            int maxSize = 3;\n\n            ModulatorComputer.ModulatorResult<String> result = modulatorComputer.computeModulator(complete, 1, maxSize);\n\n            assertTrue(result.getSize() <= maxSize);\n        }\n\n        @ParameterizedTest\n        @ValueSource(ints = {10, 20, 30})\n        @DisplayName(\"Should handle random graphs efficiently\")\n        void testRandomGraphModulator(int size) {\n            Graph<String, DefaultEdge> graph = createRandomGraph(size, 0.2);\n\n            long startTime = System.currentTimeMillis();\n            ModulatorComputer.ModulatorResult<String> result = modulatorComputer.computeModulator(graph, 3, size / 4);\n            long duration = System.currentTimeMillis() - startTime;\n\n            assertTrue(result.getResultingTreewidth() >= 0);\n            assertTrue(result.getSize() <= size / 4);\n            assertTrue(duration < 10000); // Should complete within 10 seconds\n        }\n\n        @Test\n        @DisplayName(\"Should find better modulators with larger budgets\")\n        void testModulatorQualityImprovement() {\n            Graph<String, DefaultEdge> graph = createGridGraph(4, 4);\n\n            ModulatorComputer.ModulatorResult<String> smallResult = modulatorComputer.computeModulator(graph, 2, 2);\n            ModulatorComputer.ModulatorResult<String> largeResult = modulatorComputer.computeModulator(graph, 2, 6);\n\n            // Larger budget should achieve better or equal treewidth\n            assertTrue(largeResult.getResultingTreewidth() <= smallResult.getResultingTreewidth());\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Enhanced Parameter Computer Tests\")\n    class EnhancedParameterComputerTests {\n\n        @Test\n        @DisplayName(\"Should compute enhanced parameters for simple graph\")\n        void testSimpleGraphParameters() {\n            Graph<String, DefaultEdge> graph = createCycleGraph(5);\n\n            EnhancedParameterComputer.EnhancedParameters<String> params =\n                    parameterComputer.computeOptimalParameters(graph, 3);\n\n            assertTrue(params.getK() >= 1); // Cycle needs feedback vertex set\n            assertTrue(params.getModulatorSize() <= 3);\n            assertTrue(params.getEta() >= 0);\n            assertTrue(params.getTotalParameter() > 0);\n        }\n\n        @Test\n        @DisplayName(\"Should compute multiple parameter options\")\n        void testMultipleParameterOptions() {\n            Graph<String, DefaultEdge> graph = createRandomGraph(15, 0.3);\n\n            List<EnhancedParameterComputer.EnhancedParameters<String>> options =\n                    parameterComputer.computeMultipleParameterOptions(graph, 5, 3);\n\n            assertFalse(options.isEmpty());\n            assertTrue(options.size() <= 3);\n\n            // Options should be sorted by quality\n            for (int i = 1; i < options.size(); i++) {\n                assertTrue(\n                        options.get(i - 1).getQualityScore() <= options.get(i).getQualityScore());\n            }\n        }\n\n        @Test\n        @DisplayName(\"Should validate modulators correctly\")\n        void testModulatorValidation() {\n            Graph<String, DefaultEdge> graph = createPathGraph(8);\n            Set<String> emptyModulator = new HashSet<>();\n            Set<String> singleVertexModulator = Set.of(\"V3\");\n\n            assertTrue(parameterComputer.validateModulator(graph, emptyModulator, 1));\n            assertTrue(parameterComputer.validateModulator(graph, singleVertexModulator, 1));\n        }\n\n        @Test\n        @DisplayName(\"Should compute kernel size bounds correctly\")\n        void testKernelSizeBounds() {\n            Graph<String, DefaultEdge> graph = createCycleGraph(4);\n\n            EnhancedParameterComputer.EnhancedParameters<String> params =\n                    parameterComputer.computeOptimalParameters(graph, 2, 1);\n\n            double kernelBound = params.getKernelSizeBound();\n            assertTrue(kernelBound >= 1.0);\n            assertTrue(kernelBound < Double.MAX_VALUE);\n        }\n\n        @Test\n        @DisplayName(\"Should handle edge cases gracefully\")\n        void testEdgeCases() {\n            // Empty graph\n            Graph<String, DefaultEdge> emptyGraph = new DefaultDirectedGraph<>(DefaultEdge.class);\n            EnhancedParameterComputer.EnhancedParameters<String> emptyParams =\n                    parameterComputer.computeOptimalParameters(emptyGraph, 1);\n\n            assertEquals(0, emptyParams.getK());\n            assertTrue(emptyParams.getModulator().isEmpty());\n\n            // Single vertex\n            Graph<String, DefaultEdge> singleVertex = new DefaultDirectedGraph<>(DefaultEdge.class);\n            singleVertex.addVertex(\"V0\");\n            EnhancedParameterComputer.EnhancedParameters<String> singleParams =\n                    parameterComputer.computeOptimalParameters(singleVertex, 1);\n\n            assertEquals(0, singleParams.getK());\n            assertEquals(0, singleParams.getEta());\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Integration and Performance Tests\")\n    class IntegrationPerformanceTests {\n\n        @Test\n        @DisplayName(\"Should compute parameters for complex graphs\")\n        void testComplexGraphParameters() {\n            // Create a more complex graph structure\n            Graph<String, DefaultEdge> graph = createComplexGraph();\n\n            EnhancedParameterComputer.EnhancedParameters<String> params =\n                    parameterComputer.computeOptimalParameters(graph, 5, 2);\n\n            assertTrue(params.getK() >= 0);\n            assertTrue(params.getModulatorSize() <= 5);\n            assertTrue(params.getEta() <= 2);\n\n            // Verify kernel size bound is reasonable\n            double kernelBound = params.getKernelSizeBound();\n            assertTrue(kernelBound >= 1.0);\n        }\n\n        @Test\n        @DisplayName(\"Should handle concurrent parameter computation\")\n        void testConcurrentParameterComputation() throws InterruptedException {\n            List<Graph<String, DefaultEdge>> graphs = IntStream.range(0, 5)\n                    .mapToObj(i -> createRandomGraph(15, 0.25))\n                    .collect(java.util.stream.Collectors.toList());\n\n            List<java.util.concurrent.CompletableFuture<EnhancedParameterComputer.EnhancedParameters<String>>> futures =\n                    graphs.stream()\n                            .map(graph -> java.util.concurrent.CompletableFuture.supplyAsync(\n                                    () -> parameterComputer.computeOptimalParameters(graph, 4)))\n                            .collect(java.util.stream.Collectors.toList());\n\n            List<EnhancedParameterComputer.EnhancedParameters<String>> results = futures.stream()\n                    .map(java.util.concurrent.CompletableFuture::join)\n                    .collect(java.util.stream.Collectors.toList());\n\n            assertEquals(5, results.size());\n            results.forEach(params -> {\n                assertTrue(params.getK() >= 0);\n                assertTrue(params.getModulatorSize() <= 4);\n                assertTrue(params.getEta() >= 0);\n            });\n        }\n\n        @RepeatedTest(3)\n        @DisplayName(\"Should produce consistent results\")\n        void testConsistentResults() {\n            Graph<String, DefaultEdge> graph = createGridGraph(3, 3);\n\n            EnhancedParameterComputer.EnhancedParameters<String> params1 =\n                    parameterComputer.computeOptimalParameters(graph, 3, 2);\n            EnhancedParameterComputer.EnhancedParameters<String> params2 =\n                    parameterComputer.computeOptimalParameters(graph, 3, 2);\n\n            // Results should be deterministic for the same inputs\n            assertEquals(params1.getK(), params2.getK());\n            assertEquals(params1.getEta(), params2.getEta());\n            // Modulator might vary but should have same size and achieve same treewidth\n            assertEquals(params1.getModulatorSize(), params2.getModulatorSize());\n        }\n    }\n\n    // Helper methods for creating test graphs\n\n    private Graph<String, DefaultEdge> createTreeGraph(int size) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        for (int i = 0; i < size; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        for (int i = 1; i < size; i++) {\n            graph.addEdge(\"V\" + (i / 2), \"V\" + i); // Binary tree structure\n        }\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createCycleGraph(int size) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        for (int i = 0; i < size; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        for (int i = 0; i < size; i++) {\n            graph.addEdge(\"V\" + i, \"V\" + ((i + 1) % size));\n        }\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createCompleteGraph(int size) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        for (int i = 0; i < size; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        for (int i = 0; i < size; i++) {\n            for (int j = 0; j < size; j++) {\n                if (i != j) {\n                    graph.addEdge(\"V\" + i, \"V\" + j);\n                }\n            }\n        }\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createPathGraph(int size) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        for (int i = 0; i < size; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        for (int i = 0; i < size - 1; i++) {\n            graph.addEdge(\"V\" + i, \"V\" + (i + 1));\n        }\n\n        System.out.println(graph);\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createGridGraph(int rows, int cols) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        for (int i = 0; i < rows; i++) {\n            for (int j = 0; j < cols; j++) {\n                graph.addVertex(\"V\" + i + \"_\" + j);\n            }\n        }\n\n        // Add edges\n        for (int i = 0; i < rows; i++) {\n            for (int j = 0; j < cols; j++) {\n                String current = \"V\" + i + \"_\" + j;\n\n                // Right edge\n                if (j < cols - 1) {\n                    graph.addEdge(current, \"V\" + i + \"_\" + (j + 1));\n                }\n\n                // Down edge\n                if (i < rows - 1) {\n                    graph.addEdge(current, \"V\" + (i + 1) + \"_\" + j);\n                }\n            }\n        }\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createRandomGraph(int vertexCount, double edgeProbability) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        // Add vertices\n        for (int i = 0; i < vertexCount; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        // Add random edges\n        for (int i = 0; i < vertexCount; i++) {\n            for (int j = 0; j < vertexCount; j++) {\n                if (i != j && random.nextDouble() < edgeProbability) {\n                    graph.addEdge(\"V\" + i, \"V\" + j);\n                }\n            }\n        }\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createComplexGraph() {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        for (int i = 0; i < 12; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        // Create a complex structure with multiple cycles and high-degree vertices\n        // Central hub\n        for (int i = 1; i <= 4; i++) {\n            graph.addEdge(\"V0\", \"V\" + i);\n            graph.addEdge(\"V\" + i, \"V0\");\n        }\n\n        // Two cycles\n        for (int i = 5; i <= 7; i++) {\n            graph.addEdge(\"V\" + i, \"V\" + ((i - 5 + 1) % 3 + 5));\n        }\n\n        for (int i = 8; i <= 11; i++) {\n            graph.addEdge(\"V\" + i, \"V\" + ((i - 8 + 1) % 4 + 8));\n        }\n\n        // Connections between components\n        graph.addEdge(\"V1\", \"V5\");\n        graph.addEdge(\"V2\", \"V8\");\n        graph.addEdge(\"V7\", \"V10\");\n\n        return graph;\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/vertex/kernelized/ParameterComputerExample.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport java.util.Set;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\n\npublic class ParameterComputerExample {\n\n    public static void main(String[] args) {\n        // Create a sample directed graph with cycles\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // Add vertices\n        for (int i = 0; i < 6; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        // Add edges to create cycles\n        graph.addEdge(\"V0\", \"V1\");\n        graph.addEdge(\"V1\", \"V2\");\n        graph.addEdge(\"V2\", \"V0\"); // First cycle\n        graph.addEdge(\"V2\", \"V3\");\n        graph.addEdge(\"V3\", \"V4\");\n        graph.addEdge(\"V4\", \"V5\");\n        graph.addEdge(\"V5\", \"V2\"); // Second cycle\n\n        // Create parameter computer\n        ParameterComputer<String, DefaultEdge> computer = new ParameterComputer<>(new SuperTypeToken<>() {});\n\n        try {\n            // Compute parameters without modulator\n            ParameterComputer.Parameters params1 = computer.computeParameters(graph);\n            System.out.println(\"Parameters without modulator: \" + params1);\n\n            // Compute parameters with a modulator\n            Set<String> modulator = Set.of(\"V2\"); // V2 connects both cycles\n            ParameterComputer.Parameters params2 = computer.computeParameters(graph, modulator);\n            System.out.println(\"Parameters with modulator {V2}: \" + params2);\n\n            // Find optimal modulator automatically\n            ParameterComputer.Parameters params3 = computer.computeParametersWithOptimalModulator(graph, 2);\n            System.out.println(\"Parameters with optimal modulator: \" + params3);\n\n        } finally {\n            computer.shutdown();\n        }\n    }\n}\n"
  },
  {
    "path": "graph-algorithms/src/test/java/org/hjug/feedback/vertex/kernelized/ParameterComputerTest.java",
    "content": "package org.hjug.feedback.vertex.kernelized;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport java.util.*;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.IntStream;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedGraph;\nimport org.jgrapht.graph.DefaultEdge;\nimport org.junit.jupiter.api.*;\nimport org.junit.jupiter.api.parallel.Execution;\nimport org.junit.jupiter.api.parallel.ExecutionMode;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.ValueSource;\n\n@Execution(ExecutionMode.CONCURRENT)\nclass ParameterComputerTest {\n\n    private ParameterComputer<String, DefaultEdge> parameterComputer;\n    private TreewidthComputer<String, DefaultEdge> treewidthComputer;\n    private FeedbackVertexSetComputer<String, DefaultEdge> fvsComputer;\n    private SuperTypeToken<DefaultEdge> token;\n\n    @BeforeEach\n    void setUp() {\n        token = new SuperTypeToken<>() {};\n        parameterComputer = new ParameterComputer<>(token);\n        treewidthComputer = new TreewidthComputer<>();\n        fvsComputer = new FeedbackVertexSetComputer<>(token);\n    }\n\n    @AfterEach\n    void tearDown() {\n        parameterComputer.shutdown();\n        treewidthComputer.shutdown();\n        fvsComputer.shutdown();\n    }\n\n    @Nested\n    @DisplayName(\"Treewidth Computation Tests\")\n    class TreewidthComputationTests {\n\n        @Test\n        @DisplayName(\"Should compute eta=0 for empty graph\")\n        void testEmptyGraph() {\n            Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n            int eta = treewidthComputer.computeEta(graph, new HashSet<>());\n            assertEquals(0, eta);\n        }\n\n        @Test\n        @DisplayName(\"Should compute eta=0 for single vertex\")\n        void testSingleVertex() {\n            Graph<String, DefaultEdge> graph = createSingleVertexGraph();\n            int eta = treewidthComputer.computeEta(graph, new HashSet<>());\n            assertEquals(0, eta);\n        }\n\n        @Test\n        @DisplayName(\"Should compute eta=1 for path graph\")\n        void testPathGraph() {\n            Graph<String, DefaultEdge> graph = createPathGraph(5);\n            int eta = treewidthComputer.computeEta(graph, new HashSet<>());\n            assertEquals(1, eta);\n        }\n\n        @Test\n        @DisplayName(\"Should compute eta=2 for cycle graph\")\n        void testCycleGraph() {\n            Graph<String, DefaultEdge> graph = createCycleGraph(5);\n            int eta = treewidthComputer.computeEta(graph, new HashSet<>());\n            assertTrue(eta >= 2);\n        }\n\n        @Test\n        @DisplayName(\"Should handle modulator removal correctly\")\n        void testModulatorRemoval() {\n            Graph<String, DefaultEdge> graph = createCompleteGraph(5);\n            Set<String> modulator = Set.of(\"V0\", \"V1\");\n\n            int etaWithModulator = treewidthComputer.computeEta(graph, modulator);\n            int etaWithoutModulator = treewidthComputer.computeEta(graph, new HashSet<>());\n\n            assertTrue(etaWithModulator <= etaWithoutModulator);\n        }\n\n        @ParameterizedTest\n        @ValueSource(ints = {10, 25, 50})\n        @DisplayName(\"Should handle random graphs efficiently\")\n        void testRandomGraphTreewidth(int size) {\n            Graph<String, DefaultEdge> graph = createRandomGraph(size, 0.3);\n\n            long startTime = System.currentTimeMillis();\n            int eta = treewidthComputer.computeEta(graph, new HashSet<>());\n            long duration = System.currentTimeMillis() - startTime;\n\n            assertTrue(eta >= 0);\n            assertTrue(eta < size);\n            assertTrue(duration < 5000); // Should complete within 5 seconds\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Feedback Vertex Set Computation Tests\")\n    class FeedbackVertexSetComputationTests {\n\n        @Test\n        @DisplayName(\"Should compute k=0 for acyclic graph\")\n        void testAcyclicGraph() {\n            Graph<String, DefaultEdge> graph = createPathGraph(5);\n            int k = fvsComputer.computeK(graph);\n            assertEquals(0, k);\n        }\n\n        @Test\n        @DisplayName(\"Should compute k=1 for simple cycle\")\n        void testSimpleCycle() {\n            Graph<String, DefaultEdge> graph = createCycleGraph(4);\n            int k = fvsComputer.computeK(graph);\n            assertEquals(1, k);\n        }\n\n        @Test\n        @DisplayName(\"Should handle self-loops correctly\")\n        void testSelfLoops() {\n            Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n            graph.addVertex(\"A\");\n            graph.addEdge(\"A\", \"A\");\n\n            int k = fvsComputer.computeK(graph);\n            assertEquals(1, k);\n        }\n\n        @Test\n        @DisplayName(\"Should handle multiple cycles\")\n        void testMultipleCycles() {\n            Graph<String, DefaultEdge> graph = createMultipleCyclesGraph();\n            int k = fvsComputer.computeK(graph);\n            assertEquals(1, k); // Removing node C breaks both cycles\n        }\n\n        @Test\n        @DisplayName(\"Should handle disconnected components\")\n        void testDisconnectedComponents() {\n            Graph<String, DefaultEdge> graph = createDisconnectedCyclesGraph();\n            int k = fvsComputer.computeK(graph);\n            assertTrue(k >= 2); // Each cycle needs at least one vertex removed\n        }\n\n        @ParameterizedTest\n        @ValueSource(ints = {20, 50, 100})\n        @DisplayName(\"Should handle large random graphs\")\n        void testLargeRandomGraphs(int size) {\n            Graph<String, DefaultEdge> graph = createRandomGraph(size, 0.15);\n\n            long startTime = System.currentTimeMillis();\n            int k = fvsComputer.computeK(graph);\n            long duration = System.currentTimeMillis() - startTime;\n\n            assertTrue(k >= 0);\n            assertTrue(k <= size);\n            assertTrue(duration < 30000); // Should complete within 30 seconds\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Parameter Computer Integration Tests\")\n    class ParameterComputerIntegrationTests {\n\n        @Test\n        @DisplayName(\"Should compute valid parameters for simple graphs\")\n        void testSimpleGraphParameters() {\n            Graph<String, DefaultEdge> graph = createCycleGraph(4);\n            ParameterComputer.Parameters params = parameterComputer.computeParameters(graph);\n\n            assertTrue(params.getK() >= 1);\n            assertTrue(params.getEta() >= 0);\n            assertTrue(params.getModulatorSize() >= 0);\n        }\n\n        @Test\n        @DisplayName(\"Should compute parameters with modulator\")\n        void testParametersWithModulator() {\n            Graph<String, DefaultEdge> graph = createCompleteGraph(6);\n            Set<String> modulator = Set.of(\"V0\", \"V1\");\n\n            ParameterComputer.Parameters params = parameterComputer.computeParameters(graph, modulator);\n\n            assertEquals(2, params.getModulatorSize());\n            assertTrue(params.getK() >= 0);\n            assertTrue(params.getEta() >= 0);\n        }\n\n        @Test\n        @DisplayName(\"Should find optimal modulator\")\n        void testOptimalModulatorFinding() {\n            Graph<String, DefaultEdge> graph = createStarGraph(8);\n\n            ParameterComputer.Parameters params = parameterComputer.computeParametersWithOptimalModulator(graph, 2);\n\n            assertTrue(params.getModulatorSize() <= 2);\n            assertTrue(params.getEta() >= 0);\n        }\n\n        @RepeatedTest(5)\n        @DisplayName(\"Should produce consistent results\")\n        void testConsistentResults() {\n            Graph<String, DefaultEdge> graph = createRandomGraph(30, 0.2);\n\n            ParameterComputer.Parameters params1 = parameterComputer.computeParameters(graph);\n            ParameterComputer.Parameters params2 = parameterComputer.computeParameters(graph);\n\n            // Results should be deterministic for the same graph\n            assertEquals(params1.getK(), params2.getK());\n            assertEquals(params1.getEta(), params2.getEta());\n        }\n    }\n\n    @Nested\n    @DisplayName(\"Multithreading and Performance Tests\")\n    class MultithreadingPerformanceTests {\n\n        @Test\n        @DisplayName(\"Should handle concurrent parameter computation\")\n        void testConcurrentParameterComputation() throws InterruptedException {\n            List<Graph<String, DefaultEdge>> graphs = IntStream.range(0, 10)\n                    .mapToObj(i -> createRandomGraph(20, 0.25))\n                    .collect(java.util.stream.Collectors.toList());\n\n            List<CompletableFuture<ParameterComputer.Parameters>> futures = graphs.stream()\n                    .map(graph -> CompletableFuture.supplyAsync(() -> parameterComputer.computeParameters(graph)))\n                    .collect(java.util.stream.Collectors.toList());\n\n            List<ParameterComputer.Parameters> results =\n                    futures.stream().map(CompletableFuture::join).collect(java.util.stream.Collectors.toList());\n\n            assertEquals(10, results.size());\n            results.forEach(params -> {\n                assertTrue(params.getK() >= 0);\n                assertTrue(params.getEta() >= 0);\n            });\n        }\n\n        @Test\n        @DisplayName(\"Should scale with parallelism level\")\n        void testScalingWithParallelism() {\n            Graph<String, DefaultEdge> graph = createRandomGraph(100, 0.1);\n\n            // Test with different parallelism levels\n            for (int parallelism : Arrays.asList(1, 2, 4)) {\n                ParameterComputer<String, DefaultEdge> computer = new ParameterComputer<>(token, parallelism);\n\n                long startTime = System.currentTimeMillis();\n                ParameterComputer.Parameters params = computer.computeParameters(graph);\n                long duration = System.currentTimeMillis() - startTime;\n\n                assertTrue(params.getK() >= 0);\n                assertTrue(duration < 35000); // Reasonable time limit\n\n                computer.shutdown();\n            }\n        }\n    }\n\n    // Helper methods for creating test graphs\n\n    private Graph<String, DefaultEdge> createSingleVertexGraph() {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n        graph.addVertex(\"V0\");\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createPathGraph(int length) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        for (int i = 0; i < length; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        for (int i = 0; i < length - 1; i++) {\n            graph.addEdge(\"V\" + i, \"V\" + (i + 1));\n        }\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createCycleGraph(int size) {\n        Graph<String, DefaultEdge> graph = createPathGraph(size);\n        graph.addEdge(\"V\" + (size - 1), \"V0\");\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createCompleteGraph(int size) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        for (int i = 0; i < size; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        for (int i = 0; i < size; i++) {\n            for (int j = 0; j < size; j++) {\n                if (i != j) {\n                    graph.addEdge(\"V\" + i, \"V\" + j);\n                }\n            }\n        }\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createStarGraph(int size) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        graph.addVertex(\"center\");\n        for (int i = 0; i < size; i++) {\n            graph.addVertex(\"V\" + i);\n            graph.addEdge(\"center\", \"V\" + i);\n            graph.addEdge(\"V\" + i, \"center\");\n        }\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createMultipleCyclesGraph() {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // First cycle: A -> B -> C -> A\n        graph.addVertex(\"A\");\n        graph.addVertex(\"B\");\n        graph.addVertex(\"C\");\n        graph.addEdge(\"A\", \"B\");\n        graph.addEdge(\"B\", \"C\");\n        graph.addEdge(\"C\", \"A\");\n\n        // Second cycle: C -> D -> E -> C (overlapping)\n        graph.addVertex(\"D\");\n        graph.addVertex(\"E\");\n        graph.addEdge(\"C\", \"D\");\n        graph.addEdge(\"D\", \"E\");\n        graph.addEdge(\"E\", \"C\");\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createDisconnectedCyclesGraph() {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n\n        // First cycle\n        graph.addVertex(\"A1\");\n        graph.addVertex(\"A2\");\n        graph.addVertex(\"A3\");\n        graph.addEdge(\"A1\", \"A2\");\n        graph.addEdge(\"A2\", \"A3\");\n        graph.addEdge(\"A3\", \"A1\");\n\n        // Second cycle (disconnected)\n        graph.addVertex(\"B1\");\n        graph.addVertex(\"B2\");\n        graph.addVertex(\"B3\");\n        graph.addEdge(\"B1\", \"B2\");\n        graph.addEdge(\"B2\", \"B3\");\n        graph.addEdge(\"B3\", \"B1\");\n\n        return graph;\n    }\n\n    private Graph<String, DefaultEdge> createRandomGraph(int vertexCount, double edgeProbability) {\n        Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);\n        ThreadLocalRandom random = ThreadLocalRandom.current();\n\n        // Add vertices\n        for (int i = 0; i < vertexCount; i++) {\n            graph.addVertex(\"V\" + i);\n        }\n\n        // Add random edges\n        for (int i = 0; i < vertexCount; i++) {\n            for (int j = 0; j < vertexCount; j++) {\n                if (i != j && random.nextDouble() < edgeProbability) {\n                    graph.addEdge(\"V\" + i, \"V\" + j);\n                }\n            }\n        }\n\n        return graph;\n    }\n}\n"
  },
  {
    "path": "graph-data-generator/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.graphdatagenerator</groupId>\n    <artifactId>graph-data-generator</artifactId>\n\n    <name>RefactorFirst Graph Data Generator</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.hjug.refactorfirst.costbenefitcalculator</groupId>\n            <artifactId>cost-benefit-calculator</artifactId>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "graph-data-generator/src/main/java/org/hjug/gdg/GraphDataGenerator.java",
    "content": "package org.hjug.gdg;\n\nimport java.util.List;\nimport org.hjug.cbc.RankedDisharmony;\n\npublic class GraphDataGenerator {\n\n    public String getGodClassScriptStart() {\n        return \"      google.charts.load('current', {'packages':['corechart']});\\n\"\n                + \"      google.charts.setOnLoadCallback(drawSeriesChart);\\n\"\n                + \"\\n\"\n                + \"    function drawSeriesChart() {\\n\"\n                + \"\\n\"\n                + \"      var data = google.visualization.arrayToDataTable([\";\n    }\n\n    public String getGodClassScriptEnd() {\n        return \"]);\\n\" + \"\\n\"\n                + \"      var options = {\\n\"\n                + \"        title: 'Priority Ranking for Refactoring God Classes - ' +\\n\"\n                + \"               'Start with Priority 1',\\n\"\n                + \"        height: 900, \"\n                + \"        width: 1200, \"\n                + \"        explorer: {}, \"\n                + \"        hAxis: {title: 'Effort'},\\n\"\n                + \"        vAxis: {title: 'Change Proneness'},\\n\"\n                + \"        colorAxis: {colors: ['green', 'red']},\\n\"\n                + \"        bubble: {textStyle: {fontSize: 11}}      };\\n\"\n                + \"\\n\"\n                + \"      var chart = new google.visualization.BubbleChart(document.getElementById('series_chart_div'));\\n\"\n                + \"      chart.draw(data, options);\\n\"\n                + \"    }\\n\";\n    }\n\n    public String getCBOScriptStart() {\n        return \"      google.charts.load('current', {'packages':['corechart']});\\n\"\n                + \"      google.charts.setOnLoadCallback(drawSeriesChart);\\n\"\n                + \"\\n\"\n                + \"    function drawSeriesChart() {\\n\"\n                + \"\\n\"\n                + \"      var data2 = google.visualization.arrayToDataTable([\";\n    }\n\n    public String getCBOScriptEnd() {\n        return \"]);\\n\" + \"\\n\"\n                + \"      var options = {\\n\"\n                + \"        title: 'Priority Ranking for Refactoring Highly Coupled Classes - ' +\\n\"\n                + \"               'Start with Priority 1',\\n\"\n                + \"        height: 900, \"\n                + \"        width: 1200, \"\n                + \"        explorer: {}, \"\n                + \"        hAxis: {title: 'Coupling Count'},\\n\"\n                + \"        vAxis: {title: 'Change Proneness'},\\n\"\n                + \"        colorAxis: {colors: ['green', 'red']},\\n\"\n                + \"        bubble: {textStyle: {fontSize: 11}}      };\\n\"\n                + \"\\n\"\n                + \"      var chart2 = new google.visualization.BubbleChart(document.getElementById('series_chart_div_2'));\\n\"\n                + \"      chart2.draw(data2, options);\\n\"\n                + \"    }\\n\";\n    }\n\n    public String generateGodClassBubbleChartData(List<RankedDisharmony> rankedDisharmonies, int maxPriority) {\n\n        StringBuilder chartData = new StringBuilder();\n        chartData.append(\"[ 'ID', 'Effort', 'Change Proneness', 'Priority', 'Priority (Visual)'], \");\n\n        for (int i = 0; i < rankedDisharmonies.size(); i++) {\n            RankedDisharmony rankedDisharmony = rankedDisharmonies.get(i);\n            chartData.append(\"[\");\n            chartData.append(\"'\");\n            chartData.append(rankedDisharmony.getFileName());\n            chartData.append(\"',\");\n            chartData.append(rankedDisharmony.getEffortRank());\n            chartData.append(\",\");\n            chartData.append(rankedDisharmony.getChangePronenessRank());\n            chartData.append(\",\");\n            chartData.append(rankedDisharmony.getPriority());\n            chartData.append(\",\");\n            chartData.append(maxPriority - rankedDisharmony.getPriority());\n            chartData.append(\"]\");\n            if (i + 1 < rankedDisharmonies.size()) {\n                chartData.append(\",\");\n            }\n        }\n        return chartData.toString();\n    }\n\n    public String generateCBOBubbleChartData(List<RankedDisharmony> rankedDisharmonies, int maxPriority) {\n\n        StringBuilder chartData = new StringBuilder();\n        chartData.append(\"[ 'ID', 'Coupling Count', 'Change Proneness', 'Priority', 'Priority (Visual)'], \");\n\n        for (int i = 0; i < rankedDisharmonies.size(); i++) {\n            RankedDisharmony rankedDisharmony = rankedDisharmonies.get(i);\n            chartData.append(\"[\");\n            chartData.append(\"'\");\n            chartData.append(rankedDisharmony.getFileName());\n            chartData.append(\"',\");\n            chartData.append(rankedDisharmony.getEffortRank());\n            chartData.append(\",\");\n            chartData.append(rankedDisharmony.getChangePronenessRank());\n            chartData.append(\",\");\n            chartData.append(rankedDisharmony.getPriority());\n            chartData.append(\",\");\n            chartData.append(maxPriority - rankedDisharmony.getPriority());\n            chartData.append(\"]\");\n            if (i + 1 < rankedDisharmonies.size()) {\n                chartData.append(\",\");\n            }\n        }\n        return chartData.toString();\n    }\n}\n"
  },
  {
    "path": "graph-data-generator/src/test/java/org/hjug/gdg/GraphDataGeneratorTest.java",
    "content": "package org.hjug.gdg;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.hjug.cbc.RankedDisharmony;\nimport org.hjug.git.ScmLogInfo;\nimport org.hjug.metrics.GodClass;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nclass GraphDataGeneratorTest {\n\n    private GraphDataGenerator graphDataGenerator;\n\n    @BeforeEach\n    public void setUp() {\n        graphDataGenerator = new GraphDataGenerator();\n    }\n\n    @Test\n    void getScriptStart() {\n        String scriptStart = \"      google.charts.load('current', {'packages':['corechart']});\\n\"\n                + \"      google.charts.setOnLoadCallback(drawSeriesChart);\\n\"\n                + \"\\n\"\n                + \"    function drawSeriesChart() {\\n\"\n                + \"\\n\"\n                + \"      var data = google.visualization.arrayToDataTable([\";\n        assertEquals(scriptStart, graphDataGenerator.getGodClassScriptStart());\n    }\n\n    @Test\n    void getScriptEnd() {\n        String scriptEnd = \"]);\\n\" + \"\\n\"\n                + \"      var options = {\\n\"\n                + \"        title: 'Priority Ranking for Refactoring God Classes - ' +\\n\"\n                + \"               'Start with Priority 1',\\n\"\n                + \"        height: 900, \"\n                + \"        width: 1200, \"\n                + \"        explorer: {}, \"\n                + \"        hAxis: {title: 'Effort'},\\n\"\n                + \"        vAxis: {title: 'Change Proneness'},\\n\"\n                + \"        colorAxis: {colors: ['green', 'red']},\\n\"\n                + \"        bubble: {textStyle: {fontSize: 11}}      };\\n\"\n                + \"\\n\"\n                + \"      var chart = new google.visualization.BubbleChart(document.getElementById('series_chart_div'));\\n\"\n                + \"      chart.draw(data, options);\\n\"\n                + \"    }\\n\";\n\n        assertEquals(scriptEnd, graphDataGenerator.getGodClassScriptEnd());\n    }\n\n    @Test\n    void generateBubbleChartDataOneDataPoint() {\n        GodClass godClass = new GodClass(\n                \"AttributeHandler\",\n                \"AttributeHandler.java\",\n                \"org.apache.myfaces.tobago.facelets\",\n                \"(WMC=77, ATFD=105, TCC=15.555999755859375)\");\n        godClass.setOverallRank(0);\n        ScmLogInfo scmLogInfo =\n                new ScmLogInfo(\"org/apache/myfaces/tobago/facelets/AttributeHandler.java\", null, 1595275997, 0, 1);\n        scmLogInfo.setChangePronenessRank(0);\n        RankedDisharmony rankedDisharmony = new RankedDisharmony(godClass, scmLogInfo);\n        rankedDisharmony.setPriority(1);\n\n        List<RankedDisharmony> rankedDisharmonies = new ArrayList<>();\n        rankedDisharmonies.add(rankedDisharmony);\n\n        String chartData = \"[ 'ID', 'Effort', 'Change Proneness', 'Priority', 'Priority (Visual)'], \"\n                + \"['AttributeHandler.java',0,0,1,0]\";\n        Assertions.assertEquals(chartData, graphDataGenerator.generateGodClassBubbleChartData(rankedDisharmonies, 1));\n    }\n\n    // Only testing correct string formatting, not data correctness\n    @Test\n    void generateBubbleChartDataTwoDataPoints() {\n        GodClass godClass = new GodClass(\n                \"AttributeHandler\",\n                \"AttributeHandler.java\",\n                \"org.apache.myfaces.tobago.facelets\",\n                \"(WMC=77, ATFD=105, TCC=15.555999755859375)\");\n        godClass.setOverallRank(0);\n        ScmLogInfo scmLogInfo =\n                new ScmLogInfo(\"org/apache/myfaces/tobago/facelets/AttributeHandler.java\", null, 1595275997, 0, 1);\n        scmLogInfo.setChangePronenessRank(0);\n        RankedDisharmony rankedDisharmony = new RankedDisharmony(godClass, scmLogInfo);\n        rankedDisharmony.setPriority(1);\n        RankedDisharmony rankedDisharmony2 = new RankedDisharmony(godClass, scmLogInfo);\n        rankedDisharmony2.setPriority(2);\n\n        List<RankedDisharmony> rankedDisharmonies = new ArrayList<>();\n        rankedDisharmonies.add(rankedDisharmony);\n        rankedDisharmonies.add(rankedDisharmony2);\n\n        String chartData = \"[ 'ID', 'Effort', 'Change Proneness', 'Priority', 'Priority (Visual)'], \"\n                + \"['AttributeHandler.java',0,0,1,0],\"\n                + \"['AttributeHandler.java',0,0,2,-1]\";\n        Assertions.assertEquals(chartData, graphDataGenerator.generateGodClassBubbleChartData(rankedDisharmonies, 1));\n    }\n}\n"
  },
  {
    "path": "jreleaser.yml",
    "content": "# Generated with JReleaser 1.22.0 at 2026-01-24T15:46:34.8940566-06:00\nproject:\n  name: RefactorFirst\n  description: Identifies cycles and God classes in a codebase and suggests which classes should be refactored first.\n  longDescription: Identifies cycles and God classes in a codebase and suggests which classes should be refactored first.\n  authors:\n    - Jim Bethancourt\n  license: Apache-2.0\n  links:\n    homepage: https://github.com/refactorfirst/RefactorFirst\n  languages:\n    java:\n      groupId: org.hjug.refactorfirst\n#      version: 11\n  inceptionYear: 2020\n\nrelease:\n  github:\n    owner: refactorfirst\n\nsigning:\n  active: ALWAYS\n  armored: true\ndeploy:\n  maven:\n    mavenCentral:\n      release-deploy:\n        active: RELEASE\n        url: https://central.sonatype.com/api/v1/publisher\n        applyMavenCentralRules: true\n        stagingRepositories:\n          - target/staging-deploy\n    pomchecker:\n      failOnError: false\n      failOnWarning: false\n      strict: false"
  },
  {
    "path": "lombok.config",
    "content": "lombok.addLombokGeneratedAnnotation = true"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>org.hjug.refactorfirst</groupId>\n    <artifactId>refactor-first</artifactId>\n    <version>0.8.1-SNAPSHOT</version>\n    <packaging>pom</packaging>\n\n    <url>https://github.com/refactorfirst/RefactorFirst</url>\n\n    <name>RefactorFirst</name>\n    <description>\n        Plugin that identifies Cycles and God classes in a codebase and suggests which classes should be refactored first.\n        Generates a graph and a table providing (hopefully) easy to understand guidance.\n        Can be used via command line, as a build plugin, or as a report plugin.\n    </description>\n\n    <licenses>\n        <license>\n            <name>Apache License 2.0</name>\n            <url>http://www.apache.org/licenses/</url>\n            <distribution>repo</distribution>\n        </license>\n    </licenses>\n\n    <developers>\n        <developer>\n            <name>Jim Bethancourt</name>\n            <email>jimbethancourt@gmail.com</email>\n            <organization>Houston Java Users Group</organization>\n            <organizationUrl>http://www.hjug.org</organizationUrl>\n            <roles>\n                <role>developer</role>\n            </roles>\n            <timezone>CST</timezone>\n        </developer>\n    </developers>\n\n    <scm>\n        <connection>scm:git:https://github.com/refactorfirst/RefactorFirst</connection>\n        <developerConnection>scm:git:https://github.com/refactorfirst/RefactorFirst</developerConnection>\n        <url>https://github.com/refactorfirst/RefactorFirst</url>\n        <tag>HEAD</tag>\n    </scm>\n\n    <issueManagement>\n        <system>GitHub</system>\n        <url>https://github.com/refactorfirst/RefactorFirst/issues</url>\n    </issueManagement>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <maven.compiler.source>11</maven.compiler.source>\n        <maven.compiler.target>11</maven.compiler.target>\n\n        <!--Compiler plugins-->\n        <!--<errorprone.version>2.22.0</errorprone.version>-->\n        <lombok.version>1.18.36</lombok.version>\n        <sonar.projectKey>jimbethancourt_RefactorFirst</sonar.projectKey>\n        <sonar.moduleKey>${project.artifactId}</sonar.moduleKey>\n        <sonar.organization>jimbethancourt-github</sonar.organization>\n        <sonar.host.url>https://sonarcloud.io</sonar.host.url>\n\n        <maven.core.version>3.9.9</maven.core.version>\n    </properties>\n\n    <modules>\n        <module>test-resources</module>\n        <module>codebase-graph-builder</module>\n        <module>graph-algorithms</module>\n        <module>change-proneness-ranker</module>\n        <module>effort-ranker</module>\n        <module>cost-benefit-calculator</module>\n        <module>graph-data-generator</module>\n        <module>refactor-first-maven-plugin</module>\n        <!--<module>refactor-first-gradle-plugin</module>-->\n        <module>coverage</module>\n        <module>report</module>\n        <module>cli</module>\n    </modules>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.hjug.refactorfirst.changepronenessranker</groupId>\n                <artifactId>change-proneness-ranker</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.hjug.refactorfirst.effortranker</groupId>\n                <artifactId>effort-ranker</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.hjug.refactorfirst.dsm</groupId>\n                <artifactId>graph-algorithms</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.hjug.refactorfirst.costbenefitcalculator</groupId>\n                <artifactId>cost-benefit-calculator</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.hjug.refactorfirst.graphdatagenerator</groupId>\n                <artifactId>graph-data-generator</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.hjug.refactorfirst.plugin</groupId>\n                <artifactId>refactor-first-maven-plugin</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.hjug.refactorfirst.report</groupId>\n                <artifactId>report</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.hjug.refactorfirst.testresources</groupId>\n                <artifactId>test-resources</artifactId>\n                <version>${project.version}</version>\n                <scope>test</scope>\n            </dependency>\n\n            <dependency>\n                <groupId>org.hjug.refactorfirst.codebasegraphbuilder</groupId>\n                <artifactId>codebase-graph-builder</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.eclipse.jgit</groupId>\n                <artifactId>org.eclipse.jgit</artifactId>\n                <version>6.10.0.202406032230-r</version>\n                <scope>compile</scope>\n            </dependency>\n\n            <dependency>\n                <groupId>org.jgrapht</groupId>\n                <artifactId>jgrapht-core</artifactId>\n                <version>1.5.2</version>\n            </dependency>\n            <dependency>\n                <groupId>org.jgrapht</groupId>\n                <artifactId>jgrapht-opt</artifactId>\n                <version>1.5.2</version>\n            </dependency>\n\n            <dependency>\n                <groupId>in.wilsonl.minifyhtml</groupId>\n                <artifactId>minify-html</artifactId>\n                <version>0.15.0</version>\n            </dependency>\n\n            <dependency>\n                <groupId>net.sourceforge.pmd</groupId>\n                <artifactId>pmd-java</artifactId>\n                <version>7.0.0-rc4</version>\n                <scope>compile</scope>\n\n                <exclusions>\n                    <exclusion>\n                        <groupId>com.beust</groupId>\n                        <artifactId>jcommander</artifactId>\n                    </exclusion>\n                    <!--\n                    Done to accommodate unknown license issue specified in\n                    https://github.com/jimbethancourt/RefactorFirst/issues/2\n                    -->\n                    <exclusion>\n                        <groupId>net.sourceforge.saxon</groupId>\n                        <artifactId>saxon</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n\n            <dependency>\n                <groupId>com.fasterxml.jackson.core</groupId>\n                <artifactId>jackson-databind</artifactId>\n                <version>2.18.3</version>\n            </dependency>\n\n            <!-- Needed to suppress CVE-2023-2976-->\n            <!-- Guava 33.2.1-jre is used by maven-core -->\n            <dependency>\n                <groupId>com.google.guava</groupId>\n                <artifactId>guava</artifactId>\n                <version>33.4.0-jre</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.apache.maven</groupId>\n                <artifactId>maven-core</artifactId>\n                <version>${maven.core.version}</version>\n                <exclusions>\n                    <exclusion>\n                        <groupId>com.google.guava</groupId>\n                        <artifactId>guava</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n            <!--\n            <dependency>\n              <groupId>com.github.mauricioaniche</groupId>\n              <artifactId>ck</artifactId>\n              <version>0.6.2</version>\n            </dependency>\n            -->\n\n            <dependency>\n                <groupId>org.slf4j</groupId>\n                <artifactId>slf4j-api</artifactId>\n                <version>2.0.17</version>\n            </dependency>\n            <dependency>\n                <groupId>org.slf4j</groupId>\n                <artifactId>slf4j-simple</artifactId>\n                <version>2.0.17</version>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n\n    <dependencies>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <version>3.4.4</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <version>5.13.3</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-params</artifactId>\n            <version>5.13.3</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-engine</artifactId>\n            <version>5.13.3</version>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n            <version>${lombok.version}</version>\n            <optional>true</optional>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-deploy-plugin</artifactId>\n                    <version>3.1.4</version>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.8.1</version>\n                <configuration>\n                    <compilerArgs>\n                        <arg>-XDcompilePolicy=simple</arg>\n                        <!--<arg>-Xplugin:ErrorProne</arg>-->\n                    </compilerArgs>\n                    <annotationProcessorPaths>\n                        <path>\n                            <groupId>org.projectlombok</groupId>\n                            <artifactId>lombok</artifactId>\n                            <version>${lombok.version}</version>\n                        </path>\n                        <!--<path>\n                                        <groupId>com.google.errorprone</groupId>\n                                        <artifactId>error_prone_core</artifactId>\n                                        <version>${errorprone.version}</version>\n                                    </path>-->\n                    </annotationProcessorPaths>\n                    <release>11</release>\n                </configuration>\n            </plugin>\n\n            <plugin>\n                <groupId>org.jacoco</groupId>\n                <artifactId>jacoco-maven-plugin</artifactId>\n                <version>0.8.14</version>\n                <executions>\n                    <!-- to avoid bugs in some situations -->\n                    <execution>\n                        <id>default-prepare-agent</id>\n                        <goals>\n                            <goal>prepare-agent</goal>\n                        </goals>\n                    </execution>\n\n                    <!-- create report during maven verify phase -->\n                    <execution>\n                        <id>report</id>\n                        <phase>verify</phase>\n                        <goals>\n                            <goal>report</goal>\n                        </goals>\n                    </execution>\n\n                    <execution>\n                        <id>csvreport</id>\n                        <phase>verify</phase>\n                        <goals>\n                            <goal>report</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n\n            <plugin>\n                <groupId>org.pitest</groupId>\n                <artifactId>pitest-maven</artifactId>\n                <version>1.16.1</version>\n                <dependencies>\n                    <dependency>\n                        <groupId>org.pitest</groupId>\n                        <artifactId>pitest-junit5-plugin</artifactId>\n                        <version>1.2.1</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n\n            <!-- From https://mkyong.com/maven/mvn-site-java-lang-classnotfoundexception-org-apache-maven-doxia-siterenderer-documentcontent/-->\n            <!--Needed to allow mvn site to work-->\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-site-plugin</artifactId>\n                <version>3.7.1</version>\n            </plugin>\n\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-project-info-reports-plugin</artifactId>\n                <version>3.0.0</version>\n            </plugin>\n\n            <!-- Security checks -->\n            <plugin>\n                <groupId>com.github.spotbugs</groupId>\n                <artifactId>spotbugs-maven-plugin</artifactId>\n                <version>4.9.2.0</version>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.github.spotbugs</groupId>\n                        <artifactId>spotbugs</artifactId>\n                        <version>4.9.3</version>\n                    </dependency>\n                </dependencies>\n                <configuration>\n                    <effort>Max</effort>\n                    <threshold>Low</threshold>\n                    <failOnError>true</failOnError>\n                    <!--<includeFilterFile>${session.executionRootDirectory}/spotbugs-security-include.xml</includeFilterFile>\n                    <excludeFilterFile>${session.executionRootDirectory}/spotbugs-security-exclude.xml</excludeFilterFile>-->\n                    <plugins>\n                        <plugin>\n                            <groupId>com.h3xstream.findsecbugs</groupId>\n                            <artifactId>findsecbugs-plugin</artifactId>\n                            <version>1.13.0</version>\n                        </plugin>\n                    </plugins>\n                </configuration>\n                <!-- Uncomment the executions declaration below\n                to fail the build when violations are found-->\n                <!--\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>check</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                -->\n            </plugin>\n            <!--TODO: Add the SNYK plugin-->\n            <!-- https://github.com/snyk/snyk-maven-plugin -->\n\n            <plugin>\n                <groupId>com.diffplug.spotless</groupId>\n                <artifactId>spotless-maven-plugin</artifactId>\n                <version>2.44.5</version>\n                <configuration>\n                    <formats>\n                        <!-- you can define as many formats as you want, each is independent -->\n                        <format>\n                            <!-- define the files to apply to -->\n                            <includes>\n                                <include>*.java</include>\n                            </includes>\n                            <!-- define the steps to apply to those files -->\n                            <trimTrailingWhitespace />\n                            <endWithNewline />\n                            <indent>\n                                <tabs>true</tabs>\n                                <spacesPerTab>4</spacesPerTab>\n                            </indent>\n                        </format>\n                    </formats>\n                    <java>\n                        <palantirJavaFormat />\n                    </java>\n                </configuration>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>apply</goal>\n                        </goals>\n                        <phase>initialize</phase>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n\n    <profiles>\n        <profile>\n            <id>local</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.owasp</groupId>\n                        <artifactId>dependency-check-maven</artifactId>\n                        <version>12.1.0</version>\n                        <configuration>\n                            <failBuildOnCVSS>8.0</failBuildOnCVSS>\n                        </configuration>\n                        <executions>\n                            <execution>\n                                <goals>\n                                    <goal>check</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n        <profile>\n            <id>snapshot-release</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-release-plugin</artifactId>\n                        <version>2.5.3</version>\n                        <configuration>\n                            <stagingRepository>https://oss.sonatype.org/content/repositories/snapshots/\n                            </stagingRepository>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n            <distributionManagement>\n                <snapshotRepository>\n                    <id>ossrh</id>\n                    <url>https://oss.sonatype.org/content/repositories/snapshots</url>\n                </snapshotRepository>\n            </distributionManagement>\n        </profile>\n        <profile>\n            <id>publish</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-source-plugin</artifactId>\n                        <version>3.4.0</version>\n                        <executions>\n                            <execution>\n                                <id>attach-sources</id>\n                                <goals>\n                                    <goal>jar</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <version>3.12.0</version>\n                        <executions>\n                            <execution>\n                                <id>attach-javadocs</id>\n                                <goals>\n                                    <goal>jar</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                    </plugin>\n                </plugins>\n            </build>\n            <distributionManagement>\n                <repository>\n                    <id>ossrh</id>\n                    <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>\n                </repository>\n                <snapshotRepository>\n                    <id>ossrh</id>\n                    <url>https://oss.sonatype.org/content/repositories/snapshots</url>\n                </snapshotRepository>\n            </distributionManagement>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "refactor-first-gradle-plugin/build.gradle",
    "content": "plugins {\n    id 'java-gradle-plugin'\n    id 'maven-publish'\n    id 'com.gradle.plugin-publish' version '0.12.0'\n}\n\nrepositories {\n    mavenCentral()\n    maven { url 'target/dependencies' }\n    mavenLocal()\n}\n\ndependencies {\n    compileOnly gradleApi()\n//    api \"org.hjug.refactorfirst.graphdatagenerator:graph-data-generator:${version}\"\n}\n\npluginBundle {\n    website = 'https://github.com/jimbethancourt/RefactorFirst' \n    vcsUrl = 'https://github.com/jimbethancourt/RefactorFirst.git' \n    tags = ['refactor', 'report']     \n}\n\ngradlePlugin {\n    plugins { \n        refactorFirstPlugin { \n            id = 'org.hjug.refactor-first' \n            displayName = 'RefactorFirst' \n            description = 'Plugin that identifies God classes in a codebase and suggests which classes should be refactored first.' \n            implementationClass = 'org.hjug.gradlereport.RefactorFirstPlugin'\n        }\n    }\n}\n"
  },
  {
    "path": "refactor-first-gradle-plugin/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.7-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "refactor-first-gradle-plugin/gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# 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\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "refactor-first-gradle-plugin/gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "refactor-first-gradle-plugin/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.2.0-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.plugin</groupId>\n    <artifactId>refactor-first-gradle-plugin</artifactId>\n    <packaging>pom</packaging>\n\n    <properties>\n        <gradle.executable>./gradlew</gradle.executable>\n        <gradle.tasks>build</gradle.tasks>\n        <maven.main.skip>true</maven.main.skip>\n        <maven.test.skip>true</maven.test.skip>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.hjug.refactorfirst.graphdatagenerator</groupId>\n            <artifactId>graph-data-generator</artifactId>\n        </dependency>\n    </dependencies>\n\n    <!-- See http://andresalmiray.com/running-gradle-inside-maven/ for how this works -->\n    <build>\n        <plugins>\n            <!-- copy all dependencies to target/dependencies -->\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n                <version>3.1.2</version>\n                <executions>\n                    <execution>\n                        <id>copy-dependencies</id>\n                        <phase>generate-resources</phase>\n                        <goals>\n                            <goal>copy-dependencies</goal>\n                        </goals>\n                        <inherited>false</inherited>\n                        <configuration>\n                            <useBaseVersion>true</useBaseVersion>\n                            <addParentPoms>true</addParentPoms>\n                            <copyPom>true</copyPom>\n                            <useRepositoryLayout>true</useRepositoryLayout>\n                            <outputDirectory>${project.build.directory}/dependencies</outputDirectory>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <!-- execute Gradle command -->\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>exec-maven-plugin</artifactId>\n                <version>1.6.0</version>\n                <executions>\n                    <execution>\n                        <id>gradle</id>\n                        <phase>prepare-package</phase>\n                        <configuration>\n                            <executable>${gradle.executable}</executable>\n                            <arguments>\n                                <!--argument>clean</argument-->\n                                <argument>${gradle.tasks}</argument>\n                                <argument>-Pgroup=${project.groupId}</argument>\n                                <argument>-Pversion=${project.version}</argument>\n                                <argument>-S</argument>\n                            </arguments>\n                        </configuration>\n                        <goals>\n                            <goal>exec</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n    \n    <profiles>\n        <!-- configure Gradle executable on Windows -->\n        <profile>\n            <id>windows</id>\n            <activation>\n                <os>\n                    <family>windows</family>\n                </os>\n            </activation>\n            <properties>\n                <gradle.executable>gradlew.bat</gradle.executable>\n            </properties>\n        </profile>\n    </profiles>\n</project>"
  },
  {
    "path": "refactor-first-gradle-plugin/settings.gradle",
    "content": "rootProject.name = 'refactor-first-gradle-plugin'"
  },
  {
    "path": "refactor-first-gradle-plugin/src/main/java/org/hjug/gradlereport/RefactorFirstPlugin.java",
    "content": "package org.hjug.gradlereport;\n\nimport org.gradle.api.Plugin;\nimport org.gradle.api.Project;\n\npublic class RefactorFirstPlugin implements Plugin<Project> {\n    public void apply(Project project) {\n        // impl\n    }\n}"
  },
  {
    "path": "refactor-first-maven-plugin/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.plugin</groupId>\n    <artifactId>refactor-first-maven-plugin</artifactId>\n    <packaging>maven-plugin</packaging>\n\n    <name>RefactorFirst Maven Plugin</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.hjug.refactorfirst.graphdatagenerator</groupId>\n            <artifactId>graph-data-generator</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.hjug.refactorfirst.report</groupId>\n            <artifactId>report</artifactId>\n        </dependency>\n\n        <!-- Maven Reporting -->\n        <!-- Needed to suppress CVE-2023-2976 in maven-core-->\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <!-- Needed to suppress CVE-2024-36124 in maven-core-->\n        <dependency>\n            <groupId>org.iq80.snappy</groupId>\n            <artifactId>snappy</artifactId>\n            <version>0.5</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven</groupId>\n            <artifactId>maven-core</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.maven.reporting</groupId>\n            <artifactId>maven-reporting-impl</artifactId>\n            <version>4.0.0</version>\n            <exclusions>\n                <!-- Remediates xz-1.9.jar: CVE-2022-1271 -->\n                <!-- Unused transitive dependency -->\n                <exclusion>\n                    <groupId>org.tukaani</groupId>\n                    <artifactId>xz</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven.reporting</groupId>\n            <artifactId>maven-reporting-api</artifactId>\n            <version>4.0.0</version>\n        </dependency>\n\n        <!-- plugin API and plugin-tools -->\n        <dependency>\n            <groupId>org.apache.maven</groupId>\n            <artifactId>maven-plugin-api</artifactId>\n            <version>3.9.9</version>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven.plugin-tools</groupId>\n            <artifactId>maven-plugin-annotations</artifactId>\n            <version>3.15.1</version>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <artifactId>maven-install-plugin</artifactId>\n                <version>3.1.3</version>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-plugin-plugin</artifactId>\n                <version>3.15.1</version>\n                <configuration>\n                    <goalPrefix>refactor-first</goalPrefix>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>default-descriptor</id>\n                        <phase>process-classes</phase>\n                    </execution>\n                    <execution>\n                        <id>generated-helpmojo</id>\n                        <goals>\n                            <goal>helpmojo</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n    \n</project>"
  },
  {
    "path": "refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstHtmlReport.java",
    "content": "package org.hjug.mavenreport;\n\nimport java.io.File;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.maven.plugin.AbstractMojo;\nimport org.apache.maven.plugins.annotations.LifecyclePhase;\nimport org.apache.maven.plugins.annotations.Mojo;\nimport org.apache.maven.plugins.annotations.Parameter;\nimport org.apache.maven.plugins.annotations.ResolutionScope;\nimport org.apache.maven.project.MavenProject;\nimport org.hjug.refactorfirst.report.HtmlReport;\n\n@Slf4j\n@Mojo(\n        name = \"htmlReport\",\n        defaultPhase = LifecyclePhase.SITE,\n        requiresDependencyResolution = ResolutionScope.RUNTIME,\n        requiresProject = false,\n        threadSafe = true,\n        inheritByDefault = false)\npublic class RefactorFirstHtmlReport extends AbstractMojo {\n\n    @Parameter(property = \"showDetails\")\n    private boolean showDetails = false;\n\n    @Parameter(property = \"backEdgeAnalysisCount\")\n    protected int backEdgeAnalysisCount = 50;\n\n    @Parameter(property = \"analyzeCycles\")\n    private boolean analyzeCycles = true;\n\n    @Parameter(property = \"minifyHtml\")\n    private boolean minifyHtml = false;\n\n    @Parameter(property = \"excludeTests\")\n    private boolean excludeTests = true;\n\n    /**\n     * The test source directory containing test class sources.\n     */\n    @Parameter(property = \"testSourceDirectory\")\n    private String testSourceDirectory;\n\n    @Parameter(defaultValue = \"${project.name}\")\n    private String projectName;\n\n    @Parameter(defaultValue = \"${project.version}\")\n    private String projectVersion;\n\n    @Parameter(readonly = true, defaultValue = \"${project}\")\n    private MavenProject project;\n\n    @Parameter(property = \"project.build.directory\")\n    protected File outputDirectory;\n\n    @Override\n    public void execute() {\n\n        log.info(outputDirectory.getPath());\n        HtmlReport htmlReport = new HtmlReport();\n        htmlReport.execute(\n                backEdgeAnalysisCount,\n                analyzeCycles,\n                showDetails,\n                minifyHtml,\n                excludeTests,\n                testSourceDirectory,\n                projectName,\n                projectVersion,\n                project.getBasedir(),\n                project.getModel()\n                        .getReporting()\n                        .getOutputDirectory()\n                        .replace(\"${project.basedir}\" + File.separator, \"\"));\n    }\n}\n"
  },
  {
    "path": "refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenCsvReport.java",
    "content": "package org.hjug.mavenreport;\n\nimport java.io.File;\nimport org.apache.maven.plugin.AbstractMojo;\nimport org.apache.maven.plugins.annotations.LifecyclePhase;\nimport org.apache.maven.plugins.annotations.Mojo;\nimport org.apache.maven.plugins.annotations.Parameter;\nimport org.apache.maven.plugins.annotations.ResolutionScope;\nimport org.apache.maven.project.MavenProject;\nimport org.hjug.refactorfirst.report.CsvReport;\n\n@Mojo(\n        name = \"csvreport\",\n        defaultPhase = LifecyclePhase.SITE,\n        requiresDependencyResolution = ResolutionScope.RUNTIME,\n        requiresProject = false,\n        threadSafe = true,\n        inheritByDefault = false)\npublic class RefactorFirstMavenCsvReport extends AbstractMojo {\n\n    @Parameter(property = \"showDetails\")\n    private boolean showDetails = false;\n\n    @Parameter(defaultValue = \"${project.name}\")\n    private String projectName;\n\n    @Parameter(defaultValue = \"${project.version}\")\n    private String projectVersion;\n\n    @Parameter(readonly = true, defaultValue = \"${project}\")\n    private MavenProject project;\n\n    @Parameter(property = \"project.build.directory\")\n    protected File outputDirectory;\n\n    @Override\n    public void execute() {\n        CsvReport csvReport = new CsvReport();\n        csvReport.execute(\n                showDetails,\n                projectName,\n                projectVersion,\n                project.getModel()\n                        .getReporting()\n                        .getOutputDirectory()\n                        .replace(\"${project.basedir}\" + File.separator, \"\"),\n                project.getBasedir());\n    }\n}\n"
  },
  {
    "path": "refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenJsonReport.java",
    "content": "package org.hjug.mavenreport;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.io.File;\nimport org.apache.maven.plugin.AbstractMojo;\nimport org.apache.maven.plugins.annotations.LifecyclePhase;\nimport org.apache.maven.plugins.annotations.Mojo;\nimport org.apache.maven.plugins.annotations.Parameter;\nimport org.apache.maven.plugins.annotations.ResolutionScope;\nimport org.apache.maven.project.MavenProject;\nimport org.hjug.refactorfirst.report.json.JsonReportExecutor;\n\n@Mojo(\n        name = \"jsonreport\",\n        defaultPhase = LifecyclePhase.SITE,\n        requiresDependencyResolution = ResolutionScope.RUNTIME,\n        requiresProject = false,\n        threadSafe = true,\n        inheritByDefault = false)\npublic class RefactorFirstMavenJsonReport extends AbstractMojo {\n    private static final String FILE_NAME = \"refactor-first-data.json\";\n\n    private static final ObjectMapper MAPPER = new ObjectMapper();\n\n    @Parameter(readonly = true, defaultValue = \"${project}\")\n    private MavenProject project;\n\n    @Override\n    public void execute() {\n        JsonReportExecutor jsonReportExecutor = new JsonReportExecutor();\n        jsonReportExecutor.execute(\n                project.getBasedir(),\n                project.getModel()\n                        .getReporting()\n                        .getOutputDirectory()\n                        .replace(\"${project.basedir}\" + File.separator, \"\"));\n    }\n}\n"
  },
  {
    "path": "refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstMavenReport.java",
    "content": "package org.hjug.mavenreport;\n\nimport java.util.*;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.maven.doxia.markup.HtmlMarkup;\nimport org.apache.maven.doxia.sink.Sink;\nimport org.apache.maven.doxia.sink.SinkEventAttributes;\nimport org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;\nimport org.apache.maven.plugins.annotations.LifecyclePhase;\nimport org.apache.maven.plugins.annotations.Mojo;\nimport org.apache.maven.plugins.annotations.Parameter;\nimport org.apache.maven.plugins.annotations.ResolutionScope;\nimport org.apache.maven.reporting.AbstractMavenReport;\nimport org.hjug.refactorfirst.report.HtmlReport;\n\n@Slf4j\n@Mojo(\n        name = \"report\",\n        defaultPhase = LifecyclePhase.SITE,\n        requiresDependencyResolution = ResolutionScope.RUNTIME,\n        requiresProject = false,\n        threadSafe = true,\n        inheritByDefault = false)\npublic class RefactorFirstMavenReport extends AbstractMavenReport {\n\n    @Parameter(property = \"showDetails\")\n    private boolean showDetails = false;\n\n    @Parameter(property = \"backEdgeAnalysisCount\")\n    protected int backEdgeAnalysisCount = 50;\n\n    @Parameter(property = \"analyzeCycles\")\n    private boolean analyzeCycles = true;\n\n    @Parameter(property = \"excludeTests\")\n    private boolean excludeTests = true;\n\n    /**\n     * The test source directory containing test class sources.\n     */\n    @Parameter(property = \"testSourceDirectory\")\n    private String testSourceDirectory;\n\n    @Parameter(defaultValue = \"${project.name}\")\n    private String projectName;\n\n    @Parameter(defaultValue = \"${project.version}\")\n    private String projectVersion;\n\n    public String getOutputName() {\n        // This report will generate simple-report.html when invoked in a project with `mvn site`\n        return \"refactor-first-report\";\n    }\n\n    public String getName(Locale locale) {\n        // Name of the report when listed in the project-reports.html page of a project\n        return \"Refactor First Report\";\n    }\n\n    public String getDescription(Locale locale) {\n        // Description of the report when listed in the project-reports.html page of a project\n        return \"Ranks the disharmonies in a codebase.  The classes that should be refactored first \"\n                + \" have the highest priority values.\";\n    }\n\n    @Override\n    public void executeReport(Locale locale) {\n        HtmlReport htmlReport = new HtmlReport();\n\n        Sink mainSink = getSink();\n        printHead(mainSink);\n        String report = htmlReport\n                .generateReport(\n                        showDetails,\n                        backEdgeAnalysisCount,\n                        analyzeCycles,\n                        excludeTests,\n                        testSourceDirectory,\n                        projectName,\n                        projectVersion,\n                        project.getBasedir())\n                .toString();\n\n        mainSink.rawText(report);\n    }\n\n    private void printHead(Sink mainSink) {\n        mainSink.head();\n        mainSink.title();\n        mainSink.text(\"Refactor First Report for \" + projectName + \" \" + projectVersion);\n        mainSink.title_();\n\n        // GH Buttons import\n        renderJsDeclaration(mainSink, \"https://buttons.github.io/buttons.js\");\n        // google chart import\n        renderJsDeclaration(mainSink, \"https://www.gstatic.com/charts/loader.js\");\n        // for DOT graph zooming\n        renderJsDeclaration(mainSink, \"https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js\");\n\n        // sigma graph imports - sigma, graphology, graphlib, and graphlib-dot\n        renderJsDeclaration(mainSink, \"https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js\");\n        renderJsDeclaration(mainSink, \"https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js\");\n\n        // may only need graphlib-dot\n        renderJsDeclaration(mainSink, \"https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js\");\n        renderJsDeclaration(mainSink, \"https://cdn.jsdelivr.net/npm/graphlib-dot@0.6.4/dist/graphlib-dot.min.js\");\n        renderJsDeclaration(mainSink, \"https://cdn.jsdelivr.net/npm/3d-force-graph\");\n\n        mainSink.head_();\n    }\n\n    /**\n     * @See https://maven.apache.org/doxia/developers/sink.html#How_to_inject_javascript_code_into_HTML\n     */\n    private void renderJsDeclaration(Sink mainSink, String scriptUrl) {\n        SinkEventAttributeSet githubButtonJS = new SinkEventAttributeSet();\n        githubButtonJS.addAttribute(SinkEventAttributes.TYPE, \"text/javascript\");\n        githubButtonJS.addAttribute(SinkEventAttributes.SRC, scriptUrl);\n        mainSink.unknown(\"script\", new Object[] {HtmlMarkup.TAG_TYPE_START}, githubButtonJS);\n        mainSink.unknown(\"script\", new Object[] {HtmlMarkup.TAG_TYPE_END}, null);\n    }\n\n    private void renderStyle(Sink mainSink) {\n        SinkEventAttributeSet githubButtonJS = new SinkEventAttributeSet();\n        githubButtonJS.addAttribute(SinkEventAttributes.SRC, HtmlReport.POPUP_STYLE);\n        mainSink.unknown(\"script\", new Object[] {HtmlMarkup.TAG_TYPE_START}, githubButtonJS);\n        mainSink.unknown(\"script\", new Object[] {HtmlMarkup.TAG_TYPE_END}, null);\n    }\n}\n"
  },
  {
    "path": "refactor-first-maven-plugin/src/main/java/org/hjug/mavenreport/RefactorFirstSimpleHtmlReport.java",
    "content": "package org.hjug.mavenreport;\n\nimport java.io.File;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.maven.plugin.AbstractMojo;\nimport org.apache.maven.plugins.annotations.LifecyclePhase;\nimport org.apache.maven.plugins.annotations.Mojo;\nimport org.apache.maven.plugins.annotations.Parameter;\nimport org.apache.maven.plugins.annotations.ResolutionScope;\nimport org.apache.maven.project.MavenProject;\nimport org.hjug.refactorfirst.report.SimpleHtmlReport;\n\n@Slf4j\n@Mojo(\n        name = \"simpleHtmlReport\",\n        defaultPhase = LifecyclePhase.SITE,\n        requiresDependencyResolution = ResolutionScope.RUNTIME,\n        requiresProject = false,\n        threadSafe = true,\n        inheritByDefault = false)\npublic class RefactorFirstSimpleHtmlReport extends AbstractMojo {\n\n    @Parameter(property = \"showDetails\")\n    private boolean showDetails = false;\n\n    @Parameter(property = \"backEdgeAnalysisCount\")\n    private int backEdgeAnalysisCount = 50;\n\n    @Parameter(property = \"analyzeCycles\")\n    private boolean analyzeCycles = true;\n\n    @Parameter(property = \"minifyHtml\")\n    private boolean minifyHtml = false;\n\n    @Parameter(property = \"excludeTests\")\n    private boolean excludeTests = true;\n\n    /**\n     * The test source directory containing test class sources.\n     */\n    @Parameter(property = \"testSourceDirectory\")\n    private String testSourceDirectory;\n\n    @Parameter(defaultValue = \"${project.name}\")\n    private String projectName;\n\n    @Parameter(defaultValue = \"${project.version}\")\n    private String projectVersion;\n\n    @Parameter(readonly = true, defaultValue = \"${project}\")\n    private MavenProject project;\n\n    @Parameter(property = \"project.build.directory\")\n    protected File outputDirectory;\n\n    @Override\n    public void execute() {\n\n        log.info(outputDirectory.getPath());\n        SimpleHtmlReport htmlReport = new SimpleHtmlReport();\n        htmlReport.execute(\n                backEdgeAnalysisCount,\n                analyzeCycles,\n                showDetails,\n                minifyHtml,\n                excludeTests,\n                testSourceDirectory,\n                projectName,\n                projectVersion,\n                project.getBasedir(),\n                project.getModel()\n                        .getReporting()\n                        .getOutputDirectory()\n                        .replace(\"${project.basedir}\" + File.separator, \"\"));\n    }\n}\n"
  },
  {
    "path": "report/.gitignore",
    "content": "target/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**/target/\n!**/src/test/**/target/\n\n### IntelliJ IDEA ###\n.idea/modules.xml\n.idea/jarRepositories.xml\n.idea/compiler.xml\n.idea/libraries/\n*.iws\n*.iml\n*.ipr\n\n### Eclipse ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### VS Code ###\n.vscode/\n\n### Mac OS ###\n.DS_Store"
  },
  {
    "path": "report/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.report</groupId>\n    <artifactId>report</artifactId>\n\n    <name>RefactorFirst Report</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.hjug.refactorfirst.graphdatagenerator</groupId>\n            <artifactId>graph-data-generator</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>in.wilsonl.minifyhtml</groupId>\n            <artifactId>minify-html</artifactId>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "report/src/main/java/org/hjug/refactorfirst/report/CsvReport.java",
    "content": "package org.hjug.refactorfirst.report;\n\nimport static org.hjug.refactorfirst.report.ReportWriter.writeReportToDisk;\n\nimport java.io.File;\nimport java.nio.file.Paths;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.cbc.CostBenefitCalculator;\nimport org.hjug.cbc.RankedDisharmony;\nimport org.hjug.git.GitLogReader;\n\n@Slf4j\npublic class CsvReport {\n\n    public void execute(\n            boolean showDetails, String projectName, String projectVersion, String outputDirectory, File baseDir) {\n        StringBuilder fileNameSB = new StringBuilder();\n        String publishedDate = createFileDateTimeFormatter().format(Instant.now());\n\n        fileNameSB\n                .append(getOutputNamePrefix())\n                .append(\"_P\")\n                .append(projectName)\n                .append(\"_PV\")\n                .append(projectVersion)\n                .append(\"_PD\")\n                .append(publishedDate)\n                .append(\".csv\");\n        String filename = fileNameSB.toString();\n\n        if (Objects.equals(projectName, \"Maven Stub Project (No POM)\")) {\n            projectName = new File(Paths.get(\"\").toAbsolutePath().toString()).getName();\n        }\n\n        log.info(\"Generating {} for {} - {} date: {}\", filename, projectName, projectVersion, publishedDate);\n\n        StringBuilder contentBuilder = new StringBuilder();\n\n        // git management\n        GitLogReader gitLogReader = new GitLogReader();\n        String projectBaseDir;\n        Optional<File> optionalGitDir;\n\n        if (baseDir != null) {\n            projectBaseDir = baseDir.getPath();\n            optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(baseDir));\n        } else {\n            projectBaseDir = Paths.get(\"\").toAbsolutePath().toString();\n            optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(new File(projectBaseDir)));\n        }\n\n        File gitDir;\n        if (optionalGitDir.isPresent()) {\n            gitDir = optionalGitDir.get();\n        } else {\n            log.info(\n                    \"Done! No Git repository found!  Please initialize a Git repository and perform an initial commit.\");\n            contentBuilder\n                    .append(\"No Git repository found in project \")\n                    .append(projectName)\n                    .append(\" \")\n                    .append(projectVersion)\n                    .append(\". \");\n            contentBuilder.append(\"Please initialize a Git repository and perform an initial commit.\");\n            writeReportToDisk(outputDirectory, filename, contentBuilder.toString());\n            return;\n        }\n\n        String parentOfGitDir = gitDir.getParentFile().getPath();\n        log.info(\"Project Base Dir: {} \", projectBaseDir);\n        log.info(\"Parent of Git Dir: {}\", parentOfGitDir);\n\n        if (!projectBaseDir.equals(parentOfGitDir)) {\n            log.warn(\"Project Base Directory does not match Git Parent Directory\");\n            contentBuilder.append(\"Project Base Directory does not match Git Parent Directory.  \"\n                    + \"Please refer to the report at the root of the site directory.\");\n            return;\n        }\n\n        // actual calcualte\n        List<RankedDisharmony> rankedDisharmonies;\n        // TODO: revisit\n        try (CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir, new HashMap<>())) {\n            costBenefitCalculator.runPmdAnalysis();\n            rankedDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues();\n        } catch (Exception e) {\n            log.error(\"Error running analysis.\");\n            throw new RuntimeException(e);\n        }\n\n        rankedDisharmonies.sort(Comparator.comparing(RankedDisharmony::getPriority));\n\n        // perfect score: no god classes\n        if (rankedDisharmonies.isEmpty()) {\n            contentBuilder\n                    .append(\"Congratulations!  \")\n                    .append(projectName)\n                    .append(\" \")\n                    .append(projectVersion)\n                    .append(\" has no God classes!\");\n            log.info(\"Done! No God classes found!\");\n\n            writeReportToDisk(outputDirectory, filename, contentBuilder.toString());\n            return;\n        }\n\n        // create Content\n        // header\n        final String[] tableHeadings = getHeaderList(showDetails);\n        addsRow(contentBuilder, tableHeadings);\n        contentBuilder.append(\"\\n\");\n        // rows\n        for (RankedDisharmony rankedDisharmony : rankedDisharmonies) {\n            final String[] rankedDisharmonyData = getDataList(rankedDisharmony, showDetails);\n\n            contentBuilder.append(projectVersion).append(\",\");\n            addsRow(contentBuilder, rankedDisharmonyData);\n            contentBuilder.append(\"eol\" + \"\\n\");\n        }\n\n        log.info(contentBuilder.toString());\n\n        writeReportToDisk(outputDirectory, filename, contentBuilder.toString());\n    }\n\n    private DateTimeFormatter createFileDateTimeFormatter() {\n        return DateTimeFormatter.ofPattern(\"yyyyMMddhhmm\")\n                .withLocale(Locale.getDefault())\n                .withZone(ZoneId.systemDefault());\n    }\n\n    private DateTimeFormatter createCsvDateTimeFormatter() {\n        return DateTimeFormatter.ISO_LOCAL_DATE_TIME\n                .withLocale(Locale.getDefault())\n                .withZone(ZoneId.systemDefault());\n    }\n\n    private String[] getDataList(RankedDisharmony rankedDisharmony, boolean showDetails) {\n        String[] simpleRankedDisharmonyData = {\n            rankedDisharmony.getFileName(),\n            rankedDisharmony.getPriority().toString(),\n            rankedDisharmony.getChangePronenessRank().toString(),\n            rankedDisharmony.getEffortRank().toString(),\n            rankedDisharmony.getWmc().toString(),\n            createCsvDateTimeFormatter().format(rankedDisharmony.getMostRecentCommitTime()),\n            rankedDisharmony.getCommitCount().toString()\n        };\n\n        String[] detailedRankedDisharmonyData = {\n            rankedDisharmony.getFileName(),\n            rankedDisharmony.getPriority().toString(),\n            rankedDisharmony.getChangePronenessRank().toString(),\n            rankedDisharmony.getEffortRank().toString(),\n            rankedDisharmony.getWmc().toString(),\n            rankedDisharmony.getWmcRank().toString(),\n            rankedDisharmony.getAtfd().toString(),\n            rankedDisharmony.getAtfdRank().toString(),\n            rankedDisharmony.getTcc().toString(),\n            rankedDisharmony.getTccRank().toString(),\n            createCsvDateTimeFormatter().format(rankedDisharmony.getFirstCommitTime()),\n            createCsvDateTimeFormatter().format(rankedDisharmony.getMostRecentCommitTime()),\n            rankedDisharmony.getCommitCount().toString(),\n            rankedDisharmony.getPath()\n        };\n\n        return showDetails ? detailedRankedDisharmonyData : simpleRankedDisharmonyData;\n    }\n\n    private String[] getHeaderList(boolean showDetails) {\n\n        final String[] simpleTableHeadings = {\n            \"Ver\",\n            \"Class\",\n            \"Priority\",\n            \"Change Proneness Rank\",\n            \"Effort Rank\",\n            \"Method Count\",\n            \"Most Recent Commit Date\",\n            \"Commit Count\"\n        };\n\n        final String[] detailedTableHeadings = {\n            \"Ver\",\n            \"Class\",\n            \"Priority\",\n            \"Change Proneness Rank\",\n            \"Effort Rank\",\n            \"WMC\",\n            \"WMC Rank\",\n            \"ATFD\",\n            \"ATFD Rank\",\n            \"TCC\",\n            \"TCC Rank\",\n            \"Date of First Commit\",\n            \"Most Recent Commit Date\",\n            \"Commit Count\",\n            \"Full Path\"\n        };\n\n        return showDetails ? detailedTableHeadings : simpleTableHeadings;\n    }\n\n    private void addsRow(StringBuilder contentBuilder, String[] rankedDisharmonyData) {\n        for (String rowData : rankedDisharmonyData) {\n            contentBuilder.append(rowData).append(\",\");\n        }\n    }\n\n    public String getOutputNamePrefix() {\n        // This report will generate simple-report.html when invoked in a project with `mvn site`\n        return \"RefFirst\";\n    }\n\n    public String getName(Locale locale) {\n        // Name of the report when listed in the project-reports.html page of a project\n        return \"Refactor First Report data\";\n    }\n\n    public String getDescription(Locale locale) {\n        // Description of the report when listed in the project-reports.html page of a project\n        return \"DRACO Ranks the disharmonies in a codebase.  The classes that should be refactored first \"\n                + \" have the highest priority values.\";\n    }\n}\n"
  },
  {
    "path": "report/src/main/java/org/hjug/refactorfirst/report/HtmlReport.java",
    "content": "package org.hjug.refactorfirst.report;\n\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.cbc.RankedCycle;\nimport org.hjug.cbc.RankedDisharmony;\nimport org.hjug.gdg.GraphDataGenerator;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n@Slf4j\npublic class HtmlReport extends SimpleHtmlReport {\n\n    int d3Threshold = 700;\n\n    // use Files.readString(Path.of(file))\n    // Created by generative AI and modified slightly\n    public static final String SUGIYAMA_SIGMA_GRAPH = \"<script>\\n\"\n            + \"function sugiyamaLayout(graph) {\\n\" + \"    var layers = [];\\n\"\n            + \"    var nodeLevels = {};\\n\"\n            + \"    var nodes = graph.nodes();\\n\"\n            + \"    //var edges = graph.edges();\\n\"\n            + \"\\n\"\n            + \"    // Step 1: Assign levels to nodes\\n\"\n            + \"    function assignLevels() {\\n\"\n            + \"        var visited = {};\\n\"\n            + \"        var stack = [];\\n\"\n            + \"\\n\"\n            + \"        function visit(node, level) {\\n\"\n            + \"            if (visited[node]) return;\\n\"\n            + \"            visited[node] = true;\\n\"\n            + \"            nodeLevels[node] = level;\\n\"\n            + \"            if (!layers[level]) layers[level] = [];\\n\"\n            + \"            layers[level].push(node);\\n\"\n            + \"            stack.push(node);\\n\"\n            + \"            graph.forEachNeighbor(node, function (neighbor) {\\n\"\n            + \"                visit(neighbor, level + 1);\\n\"\n            + \"            });\\n\"\n            + \"        }\\n\"\n            + \"\\n\"\n            + \"        nodes.forEach(function (node) {\\n\"\n            + \"            if (!visited[node]) visit(node, 0);\\n\"\n            + \"        });\\n\"\n            + \"    }\\n\"\n            + \"\\n\"\n            + \"    // Step 2: Reduce edge crossings\\n\"\n            + \"    function reduceCrossings() {\\n\"\n            + \"        for (var i = 0; i < layers.length - 1; i++) {\\n\"\n            + \"            var layer = layers[i];\\n\"\n            + \"            var nextLayer = layers[i + 1];\\n\"\n            + \"            var positions = {};\\n\"\n            + \"\\n\"\n            + \"            nextLayer.forEach(function (node, index) {\\n\"\n            + \"                positions[node] = index;\\n\"\n            + \"            });\\n\"\n            + \"\\n\"\n            + \"            layer.sort(function (a, b) {\\n\"\n            + \"                var aPos = 0, bPos = 0;\\n\"\n            + \"                graph.forEachNeighbor(a, function (neighbor) {\\n\"\n            + \"                    aPos += positions[neighbor] || 0;\\n\"\n            + \"                });\\n\"\n            + \"                graph.forEachNeighbor(b, function (neighbor) {\\n\"\n            + \"                    bPos += positions[neighbor] || 0;\\n\"\n            + \"                });\\n\"\n            + \"                return aPos - bPos;\\n\"\n            + \"            });\\n\"\n            + \"        }\\n\"\n            + \"    }\\n\"\n            + \"\\n\"\n            + \"    // Step 3: Assign positions to nodes\\n\"\n            + \"    function assignPositions() {\\n\"\n            + \"        var yStep = 100;\\n\"\n            + \"        var xStep = 2000;\\n\"\n            + \"\\n\"\n            + \"        layers.forEach(function (layer, level) {\\n\"\n            + \"            var layerWidth = layer.length * xStep;\\n\"\n            + \"            var offsetX = ((screen.width - 200) - layerWidth) / 2; // Centering the nodes\\n\"\n            + \"\\n\"\n            + \"            layer.forEach(function (node, index) {\\n\"\n            + \"                graph.setNodeAttribute(node, 'x', offsetX + index * xStep);\\n\"\n            + \"                graph.setNodeAttribute(node, 'y', -level * yStep);\\n\"\n            + \"            });\\n\"\n            + \"        });\\n\"\n            + \"    }\\n\"\n            + \"\\n\"\n            + \"    assignLevels();\\n\"\n            + \"    reduceCrossings();\\n\"\n            + \"    assignPositions();\\n\"\n            + \"}\\n\"\n            + \"\\n\"\n            + \"function renderGraph(dot) {\\n\"\n            + \"    // Parse the DOT graph using graphlib-dot\\n\"\n            + \"    const graphlibGraph = graphlibDot.read(dot);\\n\"\n            + \"\\n\"\n            + \"    // Convert graphlib graph to graphology graph\\n\"\n            + \"    const graphologyGraph = new graphology.Graph();\\n\"\n            + \"    graphlibGraph.nodes().forEach(node => {\\n\"\n            + \"        const attrs = graphlibGraph.node(node);\\n\"\n            + \"        graphologyGraph.addNode(node, {\\n\"\n            + \"            label: node,\\n\"\n            + \"            color: attrs.color,\\n\"\n            + \"            // x: Math.random(),\\n\"\n            + \"            // y: Math.random(),\\n\"\n            + \"            size: 5,\\n\"\n            + \"        });\\n\"\n            + \"    });\\n\"\n            + \"\\n\"\n            + \"    graphlibGraph.edges().forEach(edge => {\\n\"\n            + \"        const attrs = graphlibGraph.edge(edge);\\n\"\n            + \"        graphologyGraph.addEdge(edge.v, edge.w, {\\n\"\n            + \"            color: attrs.color,\\n\"\n            + \"            size: 1,\\n\"\n            + \"            type: 'arrow',\\n\"\n            + \"        });\\n\"\n            + \"    });\\n\"\n            + \"\\n\"\n            + \"    sugiyamaLayout(graphologyGraph)\\n\"\n            + \"\\n\"\n            + \"    return graphologyGraph;\\n\"\n            + \"}\\n\"\n            + \"</script>\";\n\n    public static final String FORCE_3D_GRAPH =\n            \"<script type=\\\"module\\\">\\n\" + \"// SpriteText will only work as import\\n\"\n                    + \"        // this script block requires type=module since we are using an import\\n\"\n                    + \"        import SpriteText from \\\"https://esm.sh/three-spritetext\\\";\\n\"\n                    + \"\\n\"\n                    + \"        function createForceGraph(popupId, containerName, dot) {\\n\"\n                    + \"            // Add event listener for Escape key to close the popup\\n\"\n                    + \"            document.addEventListener('keydown', function (event) {\\n\"\n                    + \"                if (event.key === 'Escape') {\\n\"\n                    + \"                    hidePopup();\\n\"\n                    + \"                }\\n\"\n                    + \"            });\\n\"\n                    + \"\\n\"\n                    + \"            document.getElementById('overlay').style.display = 'block';\\n\"\n                    + \"            document.getElementById(popupId).style.display = 'block';\\n\"\n                    + \"            var container = document.getElementById(containerName);\\n\"\n                    + \"\\n\"\n                    + \"            // Parse the DOT graph using graphlib-dot\\n\"\n                    + \"            const graphlibGraph = graphlibDot.read(dot);\\n\"\n                    + \"\\n\"\n                    + \"            var nodes = [];\\n\"\n                    + \"            var links = [];\\n\"\n                    + \"\\n\"\n                    + \"            graphlibGraph.nodes().forEach(function (node) {\\n\"\n                    + \"                var nodeData = graphlibGraph.node(node);\\n\"\n                    + \"                nodes.push({\\n\"\n                    + \"                    id: node,\\n\"\n                    + \"                    color: nodeData.color || 'white',\\n\"\n                    + \"                });\\n\"\n                    + \"            });\\n\"\n                    + \"\\n\"\n                    + \"            graphlibGraph.edges().forEach(function (edge) {\\n\"\n                    + \"                links.push({\\n\"\n                    + \"                    source: edge.v,\\n\"\n                    + \"                    target: edge.w,\\n\"\n                    + \"                    color: graphlibGraph.edge(edge).color || 'white',\\n\"\n                    + \"                    weight: graphlibGraph.edge(edge).weight,\\n\"\n                    + \"                });\\n\"\n                    + \"            });\\n\"\n                    + \"\\n\"\n                    + \"            const gData = {\\n\"\n                    + \"                nodes: nodes,\\n\"\n                    + \"                links: links\\n\"\n                    + \"            };\\n\"\n                    + \"\\n\"\n                    + \"            // cross-link node objects\\n\"\n                    + \"            gData.links.forEach(link => {\\n\"\n                    + \"                const a = gData.nodes.find(node => node.id === link.source);\\n\"\n                    + \"                const b = gData.nodes.find(node => node.id === link.target);\\n\"\n                    + \"                !a.neighbors && (a.neighbors = []);\\n\"\n                    + \"                !b.neighbors && (b.neighbors = []);\\n\"\n                    + \"                a.neighbors.push(b);\\n\"\n                    + \"                b.neighbors.push(a);\\n\"\n                    + \"\\n\"\n                    + \"                !a.links && (a.links = []);\\n\"\n                    + \"                !b.links && (b.links = []);\\n\"\n                    + \"                a.links.push(link);\\n\"\n                    + \"                b.links.push(link);\\n\"\n                    + \"            });\\n\"\n                    + \"\\n\"\n                    + \"            const Graph = new ForceGraph3D(container)\\n\"\n                    + \"                .graphData(gData)\\n\"\n                    + \"                .nodeLabel('id')\\n\"\n                    + \"                .width(container.clientWidth)\\n\"\n                    + \"                .height(container.clientHeight);\\n\"\n                    + \"\\n\"\n                    + \"            if(gData.links.length + gData.nodes.length < 4000) {\\n\"\n                    + \"                console.log(gData.links.length + gData.nodes.length);\\n\"\n                    + \"\\n\"\n                    + \"\\n\"\n                    + \"                // use node labels instead of spheres\\n\"\n                    + \"                Graph.nodeThreeObject(node => {\\n\"\n                    + \"                    const sprite = new SpriteText(node.id);\\n\"\n                    + \"                    sprite.material.depthWrite = false; // make sprite background transparent\\n\"\n                    + \"                    sprite.color = node.color;\\n\"\n                    + \"                    sprite.textHeight = 4;\\n\"\n                    + \"                    return sprite;\\n\"\n                    + \"                });\\n\"\n                    + \"\\n\"\n                    + \"                // code to display weight as link text\\n\"\n                    + \"                // may be too much for browsers to handle\\n\"\n                    + \"                // Graph\\n\"\n                    + \"                //     .linkThreeObjectExtend(true)\\n\"\n                    + \"                //     .linkThreeObject(link => {\\n\"\n                    + \"                //         // extend link with text sprite\\n\"\n                    + \"                //         const sprite = new SpriteText(`${link.weight}`);\\n\"\n                    + \"                //         sprite.color = 'lightgrey';\\n\"\n                    + \"                //         sprite.textHeight = 3;\\n\"\n                    + \"                //         return sprite;\\n\"\n                    + \"                //     })\\n\"\n                    + \"                //     .linkPositionUpdate((sprite, {start, end}) => {\\n\"\n                    + \"                //         const middlePos = Object.assign(...['x', 'y', 'z'].map(c => ({\\n\"\n                    + \"                //             [c]: start[c] + (end[c] - start[c]) / 2 // calc middle point\\n\"\n                    + \"                //         })));\\n\"\n                    + \"                //\\n\"\n                    + \"                //         // Position sprite\\n\"\n                    + \"                //         Object.assign(sprite.position, middlePos);\\n\"\n                    + \"                //     });\\n\"\n                    + \"\\n\"\n                    + \"\\n\"\n                    + \"                // code to highlight nodes & links\\n\"\n                    + \"                // TODO: enable via control - see Manipulate Link Force Distance for example\\n\"\n                    + \"                const highlightNodes = new Set();\\n\"\n                    + \"                const highlightLinks = new Set();\\n\"\n                    + \"                let hoverNode = null;\\n\"\n                    + \"                Graph\\n\"\n                    + \"                    .nodeColor(node => highlightNodes.has(node) ? node === hoverNode ? 'rgb(255,0,0,1)' : 'rgba(255,160,0,0.8)' : 'rgba(0,255,255,0.6)')\\n\"\n                    + \"                    .linkWidth(link => highlightLinks.has(link) ? 4 : 1)\\n\"\n                    + \"                    .linkDirectionalParticles(link => highlightLinks.has(link) ? 4 : 0)\\n\"\n                    + \"                    .linkDirectionalParticleWidth(4)\\n\"\n                    + \"                    .onNodeHover(node => {\\n\"\n                    + \"                        // no state change\\n\"\n                    + \"                        if ((!node && !highlightNodes.size) || (node && hoverNode === node)) return;\\n\"\n                    + \"\\n\"\n                    + \"                        highlightNodes.clear();\\n\"\n                    + \"                        highlightLinks.clear();\\n\"\n                    + \"                        if (node) {\\n\"\n                    + \"                            highlightNodes.add(node);\\n\"\n                    + \"                            node.neighbors.forEach(neighbor => highlightNodes.add(neighbor));\\n\"\n                    + \"                            node.links.forEach(link => highlightLinks.add(link));\\n\"\n                    + \"                        }\\n\"\n                    + \"\\n\"\n                    + \"                        hoverNode = node || null;\\n\"\n                    + \"\\n\"\n                    + \"                        updateHighlight(Graph);\\n\"\n                    + \"                    })\\n\"\n                    + \"                    .onLinkHover(link => {\\n\"\n                    + \"                        highlightNodes.clear();\\n\"\n                    + \"                        highlightLinks.clear();\\n\"\n                    + \"\\n\"\n                    + \"                        if (link) {\\n\"\n                    + \"                            highlightLinks.add(link);\\n\"\n                    + \"                            highlightNodes.add(link.source);\\n\"\n                    + \"                            highlightNodes.add(link.target);\\n\"\n                    + \"                        }\\n\"\n                    + \"\\n\"\n                    + \"                        updateHighlight(Graph);\\n\"\n                    + \"                    });\\n\"\n                    + \"\\n\"\n                    + \"            }\\n\"\n                    + \"        }\\n\"\n                    + \"\\n\"\n                    + \"        // used by highlighting functionality\\n\"\n                    + \"        function updateHighlight(Graph) {\\n\"\n                    + \"            // trigger update of highlighted objects in scene\\n\"\n                    + \"            Graph\\n\"\n                    + \"                .nodeColor(Graph.nodeColor())\\n\"\n                    + \"                .linkWidth(Graph.linkWidth())\\n\"\n                    + \"                .linkDirectionalParticles(Graph.linkDirectionalParticles());\\n\"\n                    + \"        }\\n\"\n                    + \"\\n\"\n                    + \"        // needed to allow the button to open the graph\\n\"\n                    + \"        window.createForceGraph = createForceGraph;\"\n                    + \"    </script>\";\n\n    // Created by generative AI and modified\n    public static final String POPUP_STYLE = \"<style>\\n\" + \"        /* Popup container */\\n\"\n            + \"        .popup {\\n\"\n            + \"            position: fixed;\\n\"\n            + \"            display: none;\\n\"\n            + \"            width: 95%;\\n\"\n            + \"            height: 95%;\\n\"\n            + \"            background-color: white;\\n\"\n            + \"            border: 1px solid #ccc;\\n\"\n            + \"            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\\n\"\n            + \"            top: 50%;\\n\"\n            + \"            left: 50%;\\n\"\n            + \"            transform: translate(-50%, -50%);\\n\"\n            + \"            z-index: 1000;\\n\"\n            + \"            padding: 20px;\\n\"\n            + \"            box-sizing: border-box;\\n\"\n            + \"        }\\n\"\n            + \"\\n\"\n            + \"        /* Popup overlay */\\n\"\n            + \"        .overlay {\\n\"\n            + \"            position: fixed;\\n\"\n            + \"            display: none;\\n\"\n            + \"            width: 100%;\\n\"\n            + \"            height: 100%;\\n\"\n            + \"            top: 0;\\n\"\n            + \"            left: 0;\\n\"\n            + \"            background: rgba(0, 0, 0, 0.5);\\n\"\n            + \"            z-index: 999;\\n\"\n            + \"        }\\n\"\n            + \"\\n\"\n            + \"        /* Close button */\\n\"\n            + \"        .close-btn {\\n\"\n            + \"            position: absolute;\\n\"\n            + \"            top: 10px;\\n\"\n            + \"            right: 10px;\\n\"\n            + \"            cursor: pointer;\\n\"\n            + \"        }\\n\"\n            + \"    </style>\";\n\n    // Created by generative AI and modified\n    public static final String POPUP_FUNCTIONS = \"<script>\\n\"\n            + \"    function showPopup(popupId, containerName, dot) {\\n\"\n            + \"        // Add event listener for Escape key to close the popup\\n\"\n            + \"        document.addEventListener('keydown', function (event) {\\n\"\n            + \"            if (event.key === 'Escape') {\\n\"\n            + \"                hidePopup();\\n\"\n            + \"            }\\n\"\n            + \"        });\"\n            + \"        \"\n            + \"        document.getElementById('overlay').style.display = 'block';\\n\"\n            + \"        document.getElementById(popupId).style.display = 'block';\\n\"\n            + \"\\n\"\n            + \"        var graph = renderGraph(dot);\\n\"\n            + \"        var container = document.getElementById(containerName);\\n\"\n            + \"\\n\"\n            + \"        // Render with Sigma.js\\n\"\n            + \"        new Sigma(graph, container);\\n\"\n            + \"    }\\n\"\n            + \"\\n\"\n            + \"    function hidePopup() {\\n\"\n            + \"        document.getElementById('overlay').style.display = 'none';\\n\"\n            + \"        var popups = document.getElementsByClassName('popup');\\n\"\n            + \"        for (var i = 0; i < popups.length; i++) {\\n\"\n            + \"            popups[i].style.display = 'none';\\n\"\n            + \"        }\\n\"\n            + \"\\n\"\n            + \"        // Clear the graph containers to remove the previous graphs\\n\"\n            + \"        var containers = document.querySelectorAll('[id^=\\\"graph-container\\\"]');\\n\"\n            + \"        containers.forEach(function(container) {\\n\"\n            + \"            while (container.firstChild) {\\n\"\n            + \"                container.removeChild(container.firstChild);\\n\"\n            + \"            }\\n\"\n            + \"        });\\n\"\n            + \"// Remove the Escape key event listener\\n\"\n            + \"            document.removeEventListener('keydown', function (event) {\\n\"\n            + \"                if (event.key === 'Escape') {\\n\"\n            + \"                    hidePopup();\\n\"\n            + \"                }\\n\"\n            + \"            });\"\n            + \"    }\\n\"\n            + \"</script>\";\n\n    private static final String GOD_CLASS_CHART_LEGEND =\n            \"       <h2>God Class Chart Legend:</h2>\" + \"       <table border=\\\"5px\\\">\\n\"\n                    + \"          <tbody>\\n\"\n                    + \"            <tr><td><strong>X-Axis:</strong> Effort to refactor to a non-God class</td></tr>\\n\"\n                    + \"            <tr><td><strong>Y-Axis:</strong> Relative churn</td></tr>\\n\"\n                    + \"            <tr><td><strong>Color:</strong> Priority of what to fix first</td></tr>\\n\"\n                    + \"            <tr><td><strong>Circle size:</strong> Priority (Visual) of what to fix first</td></tr>\\n\"\n                    + \"          </tbody>\\n\"\n                    + \"        </table>\"\n                    + \"        <br/>\";\n\n    private static final String COUPLING_BETWEEN_OBJECT_CHART_LEGEND =\n            \"       <h2>Coupling Between Objects Chart Legend:</h2>\" + \"       <table border=\\\"5px\\\">\\n\"\n                    + \"          <tbody>\\n\"\n                    + \"            <tr><td><strong>X-Axis:</strong> Number of objects the class is coupled to</td></tr>\\n\"\n                    + \"            <tr><td><strong>Y-Axis:</strong> Relative churn</td></tr>\\n\"\n                    + \"            <tr><td><strong>Color:</strong> Priority of what to fix first</td></tr>\\n\"\n                    + \"            <tr><td><strong>Circle size:</strong> Priority (Visual) of what to fix first</td></tr>\\n\"\n                    + \"          </tbody>\\n\"\n                    + \"        </table>\"\n                    + \"        <br/>\";\n\n    @Override\n    public String printHead() {\n        // !Remember to update RefactorFirstMavenReport if this is modified\n        return // GH Buttons import\n        \"<script async defer src=\\\"https://buttons.github.io/buttons.js\\\"></script>\\n\"\n                // google chart import\n                + \"<script type=\\\"text/javascript\\\" src=\\\"https://www.gstatic.com/charts/loader.js\\\"></script>\\n\"\n                // d3 dot graph imports\n                //                + \"<script src=\\\"https://d3js.org/d3.v5.min.js\\\"></script>\\n\"\n                //                + \"<script\n                // src=\\\"https://cdnjs.cloudflare.com/ajax/libs/d3-graphviz/3.0.5/d3-graphviz.min.js\\\"></script>\\n\"\n                //                + \"<script\n                // src=\\\"https://unpkg.com/@hpcc-js/wasm@0.3.11/dist/index.min.js\\\"></script>\\n\"\n\n                //                + \"<script\n                // src=\\\"https://cdn.jsdelivr.net/npm/@hpcc-js/wasm-graphviz@1.7.0/dist/index.min.js\\\"></script>\\n\"\n                + \"<script src=\\\"https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js\\\"></script>\"\n\n                // sigma graph imports - sigma, graphology, graphlib, and graphlib-dot\n                + \"<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js\\\"></script>\\n\"\n                + \"<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js\\\"></script>\\n\"\n                // may only need graphlib-dot\n                + \"<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js\\\"></script>\\n\"\n                + \"<script src=\\\"https://cdn.jsdelivr.net/npm/graphlib-dot@0.6.4/dist/graphlib-dot.min.js\\\"></script>\\n\"\n                + \"<script src=\\\"https://cdn.jsdelivr.net/npm/3d-force-graph\\\"></script>\\n\";\n    }\n\n    String printScripts() {\n        return SUGIYAMA_SIGMA_GRAPH + FORCE_3D_GRAPH + POPUP_FUNCTIONS + POPUP_STYLE;\n    }\n\n    @Override\n    public String printOpenBodyTag() {\n        return \"  <body class=\\\"composite\\\">\\n\" + printOverlay();\n    }\n\n    private String printOverlay() {\n        return \"<div class=\\\"overlay\\\" id=\\\"overlay\\\" onclick=\\\"hidePopup()\\\"></div>\";\n    }\n\n    @Override\n    public String printTitle(String projectName, String projectVersion) {\n        return \"<title>Refactor First Report for \" + projectName + \" \" + projectVersion + \" </title>\\n\";\n    }\n\n    @Override\n    String renderGithubButtons() {\n        return \"<div align=\\\"center\\\">\\n\" + \"Show RefactorFirst some &#10084;&#65039;\\n\"\n                + \"<br/>\\n\"\n                + \"<a class=\\\"github-button\\\" href=\\\"https://github.com/refactorfirst/refactorfirst\\\" data-icon=\\\"octicon-star\\\" data-size=\\\"large\\\" data-show-count=\\\"true\\\" aria-label=\\\"Star refactorfirst/refactorfirst on GitHub\\\">Star</a>\\n\"\n                + \"<a class=\\\"github-button\\\" href=\\\"https://github.com/refactorfirst/refactorfirst/fork\\\" data-icon=\\\"octicon-repo-forked\\\" data-size=\\\"large\\\" data-show-count=\\\"true\\\" aria-label=\\\"Fork refactorfirst/refactorfirst on GitHub\\\">Fork</a>\\n\"\n                + \"<a class=\\\"github-button\\\" href=\\\"https://github.com/refactorfirst/refactorfirst/subscription\\\" data-icon=\\\"octicon-eye\\\" data-size=\\\"large\\\" data-show-count=\\\"true\\\" aria-label=\\\"Watch refactorfirst/refactorfirst on GitHub\\\">Watch</a>\\n\"\n                + \"<a class=\\\"github-button\\\" href=\\\"https://github.com/refactorfirst/refactorfirst/issues\\\" data-icon=\\\"octicon-issue-opened\\\" data-size=\\\"large\\\" data-show-count=\\\"false\\\" aria-label=\\\"Issue refactorfirst/refactorfirst on GitHub\\\">Issue</a>\\n\"\n                + \"<a class=\\\"github-button\\\" href=\\\"https://github.com/sponsors/jimbethancourt\\\" data-icon=\\\"octicon-heart\\\" data-size=\\\"large\\\" aria-label=\\\"Sponsor @jimbethancourt on GitHub\\\">Sponsor</a>\\n\"\n                + \"</div>\";\n    }\n\n    @Override\n    String writeGodClassGchartJs(List<RankedDisharmony> rankedDisharmonies, int maxPriority) {\n        GraphDataGenerator graphDataGenerator = new GraphDataGenerator();\n        String scriptStart = graphDataGenerator.getGodClassScriptStart();\n        String bubbleChartData = graphDataGenerator.generateGodClassBubbleChartData(rankedDisharmonies, maxPriority);\n        String scriptEnd = graphDataGenerator.getGodClassScriptEnd();\n\n        return scriptStart + bubbleChartData + scriptEnd;\n    }\n\n    @Override\n    String writeGCBOGchartJs(List<RankedDisharmony> rankedDisharmonies, int maxPriority) {\n        GraphDataGenerator graphDataGenerator = new GraphDataGenerator();\n        String scriptStart = graphDataGenerator.getCBOScriptStart();\n        String bubbleChartData = graphDataGenerator.generateCBOBubbleChartData(rankedDisharmonies, maxPriority);\n        String scriptEnd = graphDataGenerator.getCBOScriptEnd();\n\n        return scriptStart + bubbleChartData + scriptEnd;\n    }\n\n    public String getName(Locale locale) {\n        // Name of the report when listed in the project-reports.html page of a project\n        return \"Refactor First Report\";\n    }\n\n    public String getDescription(Locale locale) {\n        // Description of the report when listed in the project-reports.html page of a project\n        return \"Ranks the disharmonies in a codebase.  The classes that should be refactored first \"\n                + \" have the highest priority values.\";\n    }\n\n    @Override\n    String renderGodClassChart(List<RankedDisharmony> rankedGodClassDisharmonies, int maxGodClassPriority) {\n        StringBuilder stringBuilder = new StringBuilder();\n\n        String godClassChart = writeGodClassGchartJs(rankedGodClassDisharmonies, maxGodClassPriority - 1);\n        stringBuilder.append(\n                \"<div id=\\\"series_chart_div\\\" align=\\\"center\\\"><script>\" + godClassChart + \"</script></div>\\n\");\n        stringBuilder.append(renderGithubButtons());\n        stringBuilder.append(GOD_CLASS_CHART_LEGEND);\n\n        return stringBuilder.toString();\n    }\n\n    @Override\n    String renderCBOChart(List<RankedDisharmony> rankedCBODisharmonies, int maxCboPriority) {\n        StringBuilder stringBuilder = new StringBuilder();\n\n        String cboChart = writeGCBOGchartJs(rankedCBODisharmonies, maxCboPriority - 1);\n        stringBuilder.append(\n                \"<div id=\\\"series_chart_div_2\\\" align=\\\"center\\\"><script>\" + cboChart + \"</script></div>\\n\");\n        stringBuilder.append(renderGithubButtons());\n        stringBuilder.append(COUPLING_BETWEEN_OBJECT_CHART_LEGEND);\n        return stringBuilder.toString();\n    }\n\n    @Override\n    public String renderClassGraphVisuals() {\n        String dot = buildClassGraphDot(classGraph);\n        String classGraphName = \"classGraph\";\n\n        StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(generateGraphButtons(classGraphName, dot));\n\n        stringBuilder.append(\n                \"<div align=\\\"center\\\">Excludes classes that have no incoming and outgoing edges<br></div>\");\n\n        int classCount = classGraph.vertexSet().size();\n        int relationshipCount = classGraph.edgeSet().size();\n        stringBuilder.append(\"<div align=\\\"center\\\">Number of classes: \" + classCount + \"  Number of relationships: \"\n                + relationshipCount + \"<br></div>\");\n        if (classCount + relationshipCount < d3Threshold) {\n            stringBuilder.append(generateDotImage(classGraphName));\n        } else {\n            // revisit and add DOT SVG popup button\n            stringBuilder.append(\"<div align=\\\"center\\\">\\nSVG is too big to render quickly</div>\\n\");\n        }\n\n        return stringBuilder.toString();\n    }\n\n    private StringBuilder generateGraphButtons(String graphName, String dot) {\n        StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(\"<h1 align=\\\"center\\\">Class Map</h1>\");\n        stringBuilder.append(\"<script>\\n\");\n        stringBuilder.append(\"const \" + graphName + \"_dot = \" + dot + \"\\n\");\n        stringBuilder.append(\"</script>\\n\");\n        stringBuilder.append(generateForce3DPopup(graphName));\n        stringBuilder.append(generate2DPopup(graphName));\n        stringBuilder.append(generateHidePopup(graphName));\n\n        stringBuilder.append(\"<div align=\\\"center\\\">\\nRed lines represent relationships to remove.<br>\\n\");\n        stringBuilder.append(\"Red nodes represent classes to remove.<br>\\n\");\n        stringBuilder.append(\"Zoom in / out with your mouse wheel and click/move to drag the image.\\n\");\n        stringBuilder.append(\"</div>\\n\");\n        return stringBuilder;\n    }\n\n    private static String generateDotImage(String graphName) {\n        // revisit and add D3 popup button as well\n        return \"<div id=\\\"\" + graphName\n                + \"\\\" style=\\\"width: 95%; margin: auto; border: thin solid black\\\"></div>\\n\"\n                + \"<script type=\\\"module\\\">\\n\"\n                + \"import { Graphviz } from \\\"https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/index.js\\\";\\n\"\n                + \"    if (Graphviz) {\\n\"\n                + \"        const graphviz = await Graphviz.load();\\n\"\n                + \"        let svg = graphviz.layout(\"\n                + graphName + \"_dot, \\\"svg\\\", \\\"dot\\\");\\n\"\n                + \"        // Set desired width and height\\n\"\n                + \"\\n\"\n                + \"        // Modify the SVG string to include width and height attributes\\n\"\n                + \"        svg = svg.replace('<svg ', `<svg width=\\\"screen.width\\\" height=\\\"screen.height\\\"`);\\n\"\n                + \"\\n\"\n                + \"        document.getElementById(\\\"\"\n                + graphName + \"\\\").innerHTML = svg;\\n\" + \"\\n\"\n                + \"        // Make the SVG zoomable\\n\"\n                + \"        svgPanZoom('#\"\n                + graphName + \" svg', {\\n\" + \"            zoomEnabled: true,\\n\"\n                + \"            controlIconsEnabled: true\\n\"\n                + \"        });\\n\"\n                + \"    }\\n\" + \"</script>\\n\";\n    }\n\n    String buildClassGraphDot(Graph<String, DefaultWeightedEdge> classGraph) {\n        StringBuilder dot = new StringBuilder();\n        dot.append(\"`strict digraph G {\\n\");\n\n        for (DefaultWeightedEdge edge : classGraph.edgeSet()) {\n            renderEdge(classGraph, edge, dot);\n        }\n\n        // capture only classes that have a relationship with one or more other classes\n        Set<String> vertexesToRender = new HashSet<>();\n        for (DefaultWeightedEdge edge : classGraph.edgeSet()) {\n            String[] vertexes = extractVertexes(edge);\n            vertexesToRender.add(vertexes[0].trim());\n            vertexesToRender.add(vertexes[1].trim());\n        }\n\n        // render vertices\n        for (String vertex : vertexesToRender) {\n            String className = getClassName(vertex);\n\n            // if the vertex is a nested class and has no outgoing edges, skip it\n            if (className.contains(\"$\")\n                    && className.split(\"\\\\$\")[className.split(\"\\\\$\").length - 1].matches(\"\\\\d+\")\n                    && classGraph.outDegreeOf(vertex) == 0) {\n                log.info(\"Skipping vertex: {}\", className);\n                continue;\n            }\n\n            dot.append(className.replace(\"$\", \"_\"));\n\n            if (vertexesToRemove.contains(vertex)) {\n                dot.append(\" [color=red style=filled]\\n\");\n            }\n\n            dot.append(\";\\n\");\n        }\n\n        dot.append(\"}`;\");\n        return dot.toString();\n    }\n\n    private void renderEdge(\n            Graph<String, DefaultWeightedEdge> classGraph, DefaultWeightedEdge edge, StringBuilder dot) {\n        // render edge\n        String[] vertexes = extractVertexes(edge);\n\n        //        String start = getClassName(vertexes[0].trim()).replace(\"$\", \"_\");\n        //        String end = getClassName(vertexes[1].trim()).replace(\"$\", \"_\");\n\n        String startVertex = vertexes[0].trim();\n        String start = getClassName(startVertex.trim()).replace(\"$\", \"_\");\n        String endVertex = vertexes[1].trim();\n        String end = getClassName(endVertex.trim()).replace(\"$\", \"_\");\n\n        // if the vertex is a nested class and has no outgoing edges, skip it\n        if (start.contains(\"$\")\n                && start.split(\"\\\\$\")[startVertex.split(\"\\\\$\").length - 1].matches(\"\\\\d+\")\n                && classGraph.outDegreeOf(startVertex) == 0) {\n            log.info(\"Skipping edge: {} -> {}\", startVertex, endVertex);\n            return;\n        }\n\n        if (endVertex.contains(\"$\")\n                && endVertex.split(\"\\\\$\")[endVertex.split(\"\\\\$\").length - 1].matches(\"\\\\d+\")\n                && classGraph.outDegreeOf(endVertex) == 0) {\n            log.info(\"Skipping edge: {} -> {}\", startVertex, endVertex);\n            return;\n        }\n\n        log.info(\"Rendering edge: {} -> {}\", startVertex, endVertex);\n        dot.append(start);\n        dot.append(\" -> \");\n        dot.append(end);\n\n        // render edge attributes\n        int edgeWeight = (int) classGraph.getEdgeWeight(edge);\n        dot.append(\" [ \");\n        dot.append(\"label = \\\"\");\n        dot.append(edgeWeight);\n        dot.append(\"\\\" \");\n        dot.append(\"weight = \\\"\");\n        dot.append(edgeWeight);\n        dot.append(\"\\\"\");\n\n        if (edgesToRemove.contains(edge)) {\n            dot.append(\" color = \\\"red\\\"\");\n        }\n\n        dot.append(\" ];\\n\");\n    }\n\n    @Override\n    public String renderCycleVisuals(RankedCycle cycle) {\n        String dot = buildCycleDot(classGraph, cycle);\n\n        String cycleName = getClassName(cycle.getCycleName()).replace(\"$\", \"_\");\n\n        StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(generateGraphButtons(cycleName, dot));\n\n        if (cycle.getCycleNodes().size() + cycle.getEdgeSet().size() < d3Threshold) {\n            stringBuilder.append(generateDotImage(cycleName));\n        } else {\n            // revisit and add DOT SVG popup button\n            stringBuilder.append(\"<div align=\\\"center\\\">\\nSVG is too big to render quickly</div>\\n\");\n        }\n\n        stringBuilder.append(\"<br/>\\n\");\n        stringBuilder.append(\"<br/>\\n\");\n\n        return stringBuilder.toString();\n    }\n\n    String buildCycleDot(Graph<String, DefaultWeightedEdge> classGraph, RankedCycle cycle) {\n        StringBuilder dot = new StringBuilder();\n        dot.append(\"`strict digraph G {\\n\");\n\n        for (DefaultWeightedEdge edge : cycle.getEdgeSet()) {\n            renderEdge(classGraph, edge, dot);\n        }\n\n        // render vertices\n        for (String vertex : cycle.getVertexSet()) {\n            dot.append(getClassName(vertex).replace(\"$\", \"_\"));\n\n            if (vertexesToRemove.contains(vertex)) {\n                dot.append(\" [color=red style=filled]\\n\");\n            }\n\n            dot.append(\";\\n\");\n        }\n\n        dot.append(\"}`;\");\n\n        return dot.toString().replace(\"$\", \"_\");\n    }\n\n    String generate2DPopup(String cycleName) {\n        // Created by generative AI and modified\n        return \"<button style=\\\"display: block; margin: 0 auto;\\\" onclick=\\\"showPopup('popup-\" + cycleName\n                + \"', 'graph-container-\" + cycleName + \"', \" + cycleName + \"_dot )\\\">Show \" + cycleName\n                + \" 2D Popup</button>\\n\";\n    }\n\n    String generateForce3DPopup(String cycleName) {\n        // Created by generative AI and modified\n        return \"<button style=\\\"display: block; margin: 0 auto;\\\" onclick=\\\"createForceGraph('popup-\" + cycleName\n                + \"', 'graph-container-\" + cycleName + \"', \" + cycleName + \"_dot )\\\">Show \" + cycleName\n                + \" 3D Popup</button>\\n\";\n    }\n\n    String generateHidePopup(String cycleName) {\n        return \"<div class=\\\"popup\\\" id=\\\"popup-\" + cycleName + \"\\\">\\n\"\n                + \"<span class=\\\"close-btn\\\" onclick=\\\"hidePopup()\\\">×</span>\\n\"\n                + \"    <div id=\\\"graph-container-\" + cycleName + \"\\\" style=\\\"width: 100%; height: 100%;\\\"></div>\"\n                + \"\\n</div>\\n\";\n    }\n}\n"
  },
  {
    "path": "report/src/main/java/org/hjug/refactorfirst/report/ReportWriter.java",
    "content": "package org.hjug.refactorfirst.report;\n\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.nio.file.Files;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\npublic class ReportWriter {\n\n    public static void writeReportToDisk(\n            final String reportOutputDirectory, final String filename, final String string) {\n        final File reportOutputDir = new File(reportOutputDirectory);\n\n        if (!reportOutputDir.exists()) {\n            reportOutputDir.mkdirs();\n        }\n\n        final String pathname = reportOutputDirectory + File.separator + filename;\n\n        final File reportFile = new File(pathname);\n\n        try {\n            reportFile.createNewFile();\n        } catch (IOException e) {\n            log.error(\"Failure creating chart script file\", e);\n        }\n\n        try (BufferedWriter writer = Files.newBufferedWriter(reportFile.toPath(), Charset.defaultCharset())) {\n            writer.write(string);\n        } catch (IOException e) {\n            log.error(\"Error writing chart script file\", e);\n        }\n\n        log.info(\"Done! View the report at target/site/{}\", filename);\n    }\n}\n"
  },
  {
    "path": "report/src/main/java/org/hjug/refactorfirst/report/SimpleHtmlReport.java",
    "content": "package org.hjug.refactorfirst.report;\n\nimport static org.hjug.refactorfirst.report.ReportWriter.writeReportToDisk;\n\nimport in.wilsonl.minifyhtml.Configuration;\nimport in.wilsonl.minifyhtml.MinifyHtml;\nimport java.io.File;\nimport java.nio.file.Paths;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.FormatStyle;\nimport java.util.*;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.cbc.*;\nimport org.hjug.dsm.CircularReferenceChecker;\nimport org.hjug.feedback.SuperTypeToken;\nimport org.hjug.feedback.arc.pageRank.PageRankFAS;\nimport org.hjug.feedback.vertex.kernelized.DirectedFeedbackVertexSetResult;\nimport org.hjug.feedback.vertex.kernelized.DirectedFeedbackVertexSetSolver;\nimport org.hjug.feedback.vertex.kernelized.EnhancedParameterComputer;\nimport org.hjug.git.GitLogReader;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.AsSubgraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\n\n/**\n * Strictly HTML report that contains no JavaScript\n * Generates only tables\n */\n@Slf4j\npublic class SimpleHtmlReport {\n\n    public static final String THE_BEGINNING =\n            \"<html xmlns=\\\"http://www.w3.org/1999/xhtml\\\" xml:lang=\\\"en\\\" lang=\\\"en\\\">\\n\" + \"\\n\";\n\n    public static final String THE_END = \"</div>\\n\" + \"    </div>\\n\" + \"  </body>\\n\" + \"</html>\\n\";\n\n    public final String[] godClassSimpleTableHeadings = {\n        \"Class\",\n        \"Priority\",\n        \"Change Proneness Rank\",\n        \"Effort Rank\",\n        \"Method Count\",\n        \"Most Recent Commit Date\",\n        \"Commit Count\"\n    };\n\n    public final String[] godClassDetailedTableHeadings = {\n        \"Class\",\n        \"Priority\",\n        \"Raw Priority\",\n        \"Change Proneness Rank\",\n        \"Effort Rank\",\n        \"WMC\",\n        \"WMC Rank\",\n        \"ATFD\",\n        \"ATFD Rank\",\n        \"TCC\",\n        \"TCC Rank\",\n        \"Date of First Commit\",\n        \"Most Recent Commit Date\",\n        \"Commit Count\",\n        \"Full Path\"\n    };\n\n    public final String[] cboTableHeadings = {\n        \"Class\", \"Priority\", \"Change Proneness Rank\", \"Coupling Count\", \"Most Recent Commit Date\", \"Commit Count\"\n    };\n\n    public final String[] classCycleTableHeadings = {\"Classes\", \"Relationships\"};\n\n    Graph<String, DefaultWeightedEdge> classGraph;\n    Map<String, AsSubgraph<String, DefaultWeightedEdge>> cycles;\n    Set<String> vertexesToRemove = Set.of(); // initialize for unit tests\n    Set<DefaultWeightedEdge> edgesToRemove = Set.of();\n\n    DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)\n            .withLocale(Locale.getDefault())\n            .withZone(ZoneId.systemDefault());\n\n    private final Configuration htmlMinifierConfig = new Configuration.Builder()\n            .setKeepHtmlAndHeadOpeningTags(true)\n            .setKeepComments(false)\n            .setMinifyJs(true)\n            .setMinifyCss(true)\n            .build();\n\n    public void execute(\n            int edgeAnalysisCount,\n            boolean analyzeCycles,\n            boolean showDetails,\n            boolean minifyHtml,\n            boolean excludeTests,\n            String testSourceDirectory,\n            String projectName,\n            String projectVersion,\n            File baseDir,\n            String outputDirectory) {\n\n        String filename = getOutputName() + \".html\";\n        log.info(\"Generating {} for {} - {}\", filename, projectName, projectVersion);\n\n        StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(THE_BEGINNING);\n\n        stringBuilder.append(\"<head>\");\n        stringBuilder.append(printTitle(projectName, projectVersion));\n        stringBuilder.append(printHead());\n        stringBuilder.append(\"</head>\");\n        stringBuilder.append(generateReport(\n                showDetails,\n                edgeAnalysisCount,\n                analyzeCycles,\n                excludeTests,\n                testSourceDirectory,\n                projectName,\n                projectVersion,\n                baseDir));\n\n        stringBuilder.append(printProjectFooter());\n        stringBuilder.append(THE_END);\n\n        String reportHtml;\n        if (minifyHtml) {\n            reportHtml = MinifyHtml.minify(stringBuilder.toString(), htmlMinifierConfig);\n        } else {\n            reportHtml = stringBuilder.toString();\n        }\n        writeReportToDisk(outputDirectory, filename, reportHtml);\n        log.info(\"Done! View the report at target/site/{}\", filename);\n    }\n\n    public StringBuilder generateReport(\n            boolean showDetails,\n            int edgeAnalysisCount,\n            boolean analyzeCycles,\n            boolean excludeTests,\n            String testSourceDirectory,\n            String projectName,\n            String projectVersion,\n            File baseDir) {\n\n        if (testSourceDirectory == null || testSourceDirectory.isEmpty()) {\n            testSourceDirectory = \"src\" + File.separator + \"test\";\n        }\n\n        StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(printOpenBodyTag());\n        stringBuilder.append(printScripts());\n        stringBuilder.append(printBreadcrumbs());\n        stringBuilder.append(printProjectHeader(projectName, projectVersion));\n\n        GitLogReader gitLogReader = new GitLogReader();\n        String projectBaseDir;\n        Optional<File> optionalGitDir;\n\n        if (baseDir != null) {\n            projectBaseDir = baseDir.getPath();\n            optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(baseDir));\n        } else {\n            projectBaseDir = Paths.get(\"\").toAbsolutePath().toString();\n            optionalGitDir = Optional.ofNullable(gitLogReader.getGitDir(new File(projectBaseDir)));\n        }\n\n        File gitDir;\n        if (optionalGitDir.isPresent()) {\n            gitDir = optionalGitDir.get();\n        } else {\n            log.info(\n                    \"Done! No Git repository found!  Please initialize a Git repository and perform an initial commit.\");\n            stringBuilder\n                    .append(\"No Git repository found in project \")\n                    .append(projectName)\n                    .append(\" \")\n                    .append(projectVersion)\n                    .append(\".  \");\n            stringBuilder.append(\"Please initialize a Git repository and perform an initial commit.\");\n\n            return stringBuilder;\n        }\n\n        String parentOfGitDir = gitDir.getParentFile().getPath();\n        log.info(\"Project Base Dir: {} \", projectBaseDir);\n        log.info(\"Parent of Git Dir: {}\", parentOfGitDir);\n\n        if (!projectBaseDir.equals(parentOfGitDir)) {\n            log.warn(\"Project Base Directory does not match Git Parent Directory\");\n            stringBuilder.append(\"Project Base Directory does not match Git Parent Directory.  \"\n                    + \"Please refer to the report at the root of the site directory.\");\n            return stringBuilder;\n        }\n\n        CycleRanker cycleRanker = new CycleRanker(projectBaseDir);\n        List<RankedCycle> rankedCycles = List.of();\n        if (analyzeCycles) {\n            log.info(\"Analyzing Cycles\");\n            rankedCycles = cycleRanker.performCycleAnalysis(excludeTests, testSourceDirectory);\n        } else {\n            cycleRanker.generateClassReferencesGraph(excludeTests, testSourceDirectory);\n        }\n\n        classGraph = cycleRanker.getClassReferencesGraph();\n        cycles = new CircularReferenceChecker<String, DefaultWeightedEdge>().getCycles(classGraph);\n\n        // Identify vertexes to remove\n        log.info(\"Identifying vertexes to remove\");\n        EnhancedParameterComputer<String, DefaultWeightedEdge> enhancedParameterComputer =\n                new EnhancedParameterComputer<>(new SuperTypeToken<>() {});\n        EnhancedParameterComputer.EnhancedParameters<String> parameters =\n                enhancedParameterComputer.computeOptimalParameters(classGraph, 4);\n        DirectedFeedbackVertexSetSolver<String, DefaultWeightedEdge> vertexSolver =\n                new DirectedFeedbackVertexSetSolver<>(\n                        classGraph, parameters.getModulator(), null, parameters.getEta(), new SuperTypeToken<>() {});\n        DirectedFeedbackVertexSetResult<String> vertexSetResult = vertexSolver.solve(parameters.getK());\n        vertexesToRemove = vertexSetResult.getFeedbackVertices();\n\n        // Identify edges to remove\n        log.info(\"Identifying edges to remove\");\n        PageRankFAS<String, DefaultWeightedEdge> pageRankFAS = new PageRankFAS<>(classGraph, new SuperTypeToken<>() {});\n        edgesToRemove = pageRankFAS.computeFeedbackArcSet();\n\n        // capture the number of cycles each edge to remove is in\n        Map<DefaultWeightedEdge, Integer> edgeToRemoveCycleCounts = new HashMap<>();\n        for (DefaultWeightedEdge edgeToRemove : edgesToRemove) {\n            int cycleCount = 0;\n            for (AsSubgraph<String, DefaultWeightedEdge> cycle : cycles.values()) {\n                if (cycle.containsEdge(edgeToRemove)) {\n                    cycleCount++;\n                }\n            }\n            edgeToRemoveCycleCounts.put(edgeToRemove, cycleCount);\n        }\n\n        // int edgeWeight = (int) classGraph.getEdgeWeight(defaultWeightedEdge);\n        // map sources to CycleNodes to get paths and get churn in try/finally block below\n        Map<DefaultWeightedEdge, CycleNode> sourceNodeInfos = new HashMap<>();\n        Map<DefaultWeightedEdge, CycleNode> targetNodeInfos = new HashMap<>();\n        for (DefaultWeightedEdge defaultWeightedEdge : edgesToRemove) {\n            String edgeSource = classGraph.getEdgeSource(defaultWeightedEdge);\n            CycleNode sourceNode = cycleRanker.classToCycleNode(edgeSource);\n            sourceNodeInfos.put(defaultWeightedEdge, sourceNode);\n\n            String edgeTarget = classGraph.getEdgeTarget(defaultWeightedEdge);\n            CycleNode targetNode = cycleRanker.classToCycleNode(edgeTarget);\n            targetNodeInfos.put(defaultWeightedEdge, targetNode);\n        }\n\n        List<RankedDisharmony> rankedGodClassDisharmonies = List.of();\n        List<RankedDisharmony> rankedCBODisharmonies = List.of();\n        List<RankedDisharmony> edgeDisharmonies = List.of();\n        log.info(\"Identifying Object Oriented Disharmonies\");\n\n        try (CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(\n                projectBaseDir, cycleRanker.getCodebaseGraphDTO().getClassToSourceFilePathMapping())) {\n            costBenefitCalculator.runPmdAnalysis(excludeTests, testSourceDirectory);\n            rankedGodClassDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues();\n            rankedCBODisharmonies = costBenefitCalculator.calculateCBOCostBenefitValues();\n            edgeDisharmonies = costBenefitCalculator.calculateSourceNodeCostBenefitValues(\n                    classGraph, sourceNodeInfos, targetNodeInfos, edgeToRemoveCycleCounts, vertexesToRemove);\n\n        } catch (Exception e) {\n            log.error(\"Error running analysis.\");\n            throw new RuntimeException(e);\n        }\n\n        // TODO: Incorporate node information and guidance into Edge Infos\n        // - Source / target vertex in list of vertexes to remove\n        // - How many cycles is the edge present in\n        // - Edge weight\n        // - Provide guidance on where to move the method if one is in the list to remove\n\n        if (edgesToRemove.isEmpty()\n                && rankedGodClassDisharmonies.isEmpty()\n                && rankedCBODisharmonies.isEmpty()\n                && rankedCycles.isEmpty()) {\n            stringBuilder\n                    .append(\"Congratulations!  \")\n                    .append(projectName)\n                    .append(\" \")\n                    .append(projectVersion)\n                    .append(\" has no Back Edges, God classes, Highly Coupled Classes, or Cycles!\");\n            stringBuilder.append(renderGithubButtons());\n            log.info(\"Done! No Disharmonies found!\");\n            return stringBuilder;\n        }\n\n        if (!edgesToRemove.isEmpty()) {\n            stringBuilder.append(\"<a href=\\\"#EDGES\\\">Edges To Remove</a>\\n\");\n            stringBuilder.append(\"<br/>\\n\");\n        }\n\n        if (!rankedGodClassDisharmonies.isEmpty()) {\n            stringBuilder.append(\"<a href=\\\"#GOD\\\">God Classes</a>\\n\");\n            stringBuilder.append(\"<br/>\\n\");\n        }\n\n        if (!rankedCBODisharmonies.isEmpty()) {\n            stringBuilder.append(\"<a href=\\\"#CBO\\\">Highly Coupled Classes</a>\\n\");\n            stringBuilder.append(\"<br/>\\n\");\n        }\n\n        if (!rankedCycles.isEmpty()) {\n            stringBuilder.append(\"<a href=\\\"#CYCLES\\\">Class Cycles</a>\\n\");\n        }\n\n        log.info(\"Generating HTML Report\");\n\n        stringBuilder.append(renderClassGraphVisuals());\n        stringBuilder.append(\"<br/>\\n\");\n        stringBuilder.append(renderGithubButtons());\n\n        stringBuilder.append(\"<br/>\\n\");\n        if (!edgeDisharmonies.isEmpty()) {\n            stringBuilder.append(renderEdgeDisharmonies(edgeDisharmonies));\n            stringBuilder.append(\"<br/>\\n\" + \"<br/>\\n\" + \"<br/>\\n\" + \"<br/>\\n\" + \"<hr/>\\n\" + \"<br/>\\n\" + \"<br/>\\n\");\n        }\n\n        if (!rankedGodClassDisharmonies.isEmpty()) {\n            final String[] godClassTableHeadings =\n                    showDetails ? godClassDetailedTableHeadings : godClassSimpleTableHeadings;\n            stringBuilder.append(renderGodClassInfo(showDetails, rankedGodClassDisharmonies, godClassTableHeadings));\n            stringBuilder.append(\"<br/>\\n\" + \"<br/>\\n\" + \"<br/>\\n\" + \"<br/>\\n\" + \"<hr/>\\n\" + \"<br/>\\n\" + \"<br/>\\n\");\n        }\n\n        if (!rankedCBODisharmonies.isEmpty()) {\n            stringBuilder.append(renderHighlyCoupledClassInfo(rankedCBODisharmonies));\n            stringBuilder.append(\"<br/>\\n\" + \"<br/>\\n\" + \"<br/>\\n\" + \"<br/>\\n\" + \"<hr/>\\n\" + \"<br/>\\n\" + \"<br/>\\n\");\n        }\n\n        if (!rankedCycles.isEmpty()) {\n            stringBuilder.append(renderCycles(rankedCycles));\n        }\n\n        stringBuilder.append(\"</section>\\n\");\n\n        log.debug(stringBuilder.toString());\n        return stringBuilder;\n    }\n\n    private String renderCycles(List<RankedCycle> rankedCycles) {\n        StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(renderClassCycleSummary(rankedCycles));\n\n        rankedCycles.stream().limit(1).map(this::renderSingleCycle).forEach(stringBuilder::append);\n\n        return stringBuilder.toString();\n    }\n\n    private String renderEdgeDisharmonies(List<RankedDisharmony> edgeDisharmonies) {\n        StringBuilder stringBuilder = new StringBuilder();\n\n        stringBuilder.append(\n                \"<div style=\\\"text-align: center;\\\"><a id=\\\"EDGES\\\"><h1>Relationship Removal Priority</h1></a></div>\\n\");\n        stringBuilder.append(\"<h2 align=\\\"center\\\">Refactor Starting with Priority 1</h2>\\n\");\n        stringBuilder.append(\"<div style=\\\"text-align: center;\\\">\\n\");\n        stringBuilder.append(\"Current Cycle Count: \").append(cycles.size()).append(\"<br>\\n\");\n\n        stringBuilder\n                .append(\"Number of Relationships to Remove: \")\n                .append(edgesToRemove.size())\n                .append(\"<br>\\n\");\n        stringBuilder.append(\"Classes in bold should be broken apart\").append(\"<br>\\n\");\n        stringBuilder.append(\"</div>\\n\");\n\n        // Content\n        stringBuilder.append(\"<table align=\\\"center\\\" border=\\\"5px\\\">\\n\");\n        stringBuilder.append(\"<thead>\\n<tr>\\n\");\n        for (String heading : getEdgeDisharmonyTableHeadings()) {\n            stringBuilder.append(\"<th>\").append(heading).append(\"</th>\\n\");\n        }\n        stringBuilder.append(\"</thead>\\n\");\n\n        stringBuilder.append(\"<tbody>\\n\");\n\n        for (RankedDisharmony edge : edgeDisharmonies) {\n            stringBuilder.append(\"<tr>\\n\");\n\n            for (String rowData : getEdgeDisharmony(edge)) {\n                stringBuilder.append(drawTableCell(rowData));\n            }\n\n            stringBuilder.append(\"</tr>\\n\");\n        }\n\n        stringBuilder.append(\"</tbody>\\n\");\n        stringBuilder.append(\"</table>\\n\");\n\n        return stringBuilder.toString();\n    }\n\n    private String[] getEdgeDisharmonyTableHeadings() {\n        return new String[] {\n            \"Relationship\",\n            \"Priority\",\n            \"In Cycles\",\n            \"Edge<br>Weight\",\n            \"Source<br>Change Proneness Rank\",\n            \"Target<br>Change Proneness Rank\",\n        };\n    }\n\n    private String[] getEdgeDisharmony(RankedDisharmony edgeInfo) {\n        return new String[] {\n            renderEdge(edgeInfo.getEdge()),\n            String.valueOf(edgeInfo.getPriority()),\n            String.valueOf(edgeInfo.getCycleCount()),\n            String.valueOf(edgeInfo.getEffortRank()),\n            String.valueOf(edgeInfo.getChangePronenessRank()),\n            String.valueOf(edgeInfo.getEdgeTargetChangePronenessRank()),\n        };\n    }\n\n    private String renderClassCycleSummary(List<RankedCycle> rankedCycles) {\n        StringBuilder stringBuilder = new StringBuilder();\n\n        stringBuilder.append(\"<div style=\\\"text-align: center;\\\"><a id=\\\"CYCLES\\\"><h1>Class Cycles</h1></a></div>\\n\");\n        /*if (rankedCycles.size() > 10) {\n            stringBuilder.append(\n                    \"<div style=\\\"text-align: center;\\\">10 largest cycles are shown in the sections below</div>\\n\");\n        }*/\n\n        stringBuilder.append(\"<h2 align=\\\"center\\\">Class Cycles by the numbers:</h2>\\n\");\n        stringBuilder.append(\"<table align=\\\"center\\\" border=\\\"5px\\\">\\n\");\n\n        // Content\n        stringBuilder.append(\"<thead>\\n<tr>\\n\");\n        for (String heading : getCycleSummaryTableHeadings()) {\n            stringBuilder.append(\"<th>\").append(heading).append(\"</th>\\n\");\n        }\n        stringBuilder.append(\"</thead>\\n\");\n\n        stringBuilder.append(\"<tbody>\\n\");\n        for (RankedCycle cycle : rankedCycles) {\n            stringBuilder.append(\"<tr>\\n\");\n\n            StringBuilder edges = new StringBuilder();\n            for (DefaultWeightedEdge edge : cycle.getMinCutEdges()) {\n\n                if (edgesToRemove.contains(edge)) {\n                    stringBuilder.append(\"<strong>\");\n                    edges.append(renderEdge(edge));\n                    stringBuilder.append(\"</strong>\");\n                } else {\n                    edges.append(renderEdge(edge));\n                }\n                edges.append(\"</br>\\n\");\n            }\n\n            for (String rowData : getRankedCycleSummaryData(cycle, edges)) {\n                stringBuilder.append(drawTableCell(rowData));\n            }\n\n            stringBuilder.append(\"</tr>\\n\");\n        }\n\n        stringBuilder.append(\"</tbody>\\n\");\n        stringBuilder.append(\"</table>\\n\");\n\n        return stringBuilder.toString();\n    }\n\n    private String renderEdge(DefaultWeightedEdge edge) {\n        StringBuilder edgesToCut = new StringBuilder();\n        String[] vertexes = extractVertexes(edge);\n\n        String startVertex = vertexes[0].trim();\n        String start;\n        if (vertexesToRemove.contains(startVertex)) {\n            start = \"<strong>\" + getClassName(startVertex) + \"</strong>\";\n        } else {\n            start = getClassName(startVertex);\n        }\n\n        String endVertex = vertexes[1].trim();\n        String end;\n        if (vertexesToRemove.contains(endVertex)) {\n            end = \"<strong>\" + getClassName(endVertex) + \"</strong>\";\n        } else {\n            end = getClassName(endVertex);\n        }\n\n        // &#8594; is HTML \"Right Arrow\" code\n        return edgesToCut\n                .append(start + \" &#8594; \" + end + \" : \" + (int) classGraph.getEdgeWeight(edge))\n                .toString();\n    }\n\n    private String[] getCycleSummaryTableHeadings() {\n        return new String[] {\"Cycle Name\", \"Priority\", \"Class Count\", \"Relationship Count\" /*, \"Minimum Cuts\"*/};\n    }\n\n    private String[] getRankedCycleSummaryData(RankedCycle rankedCycle, StringBuilder edgesToCut) {\n        return new String[] {\n            // \"Cycle Name\", \"Priority\", \"Class Count\", \"Relationship Count\", \"Min Cuts\"\n            getClassName(rankedCycle.getCycleName()),\n            rankedCycle.getPriority().toString(),\n            String.valueOf(rankedCycle.getCycleNodes().size()),\n            String.valueOf(rankedCycle.getEdgeSet().size())\n        };\n    }\n\n    private String renderSingleCycle(RankedCycle cycle) {\n        StringBuilder stringBuilder = new StringBuilder();\n\n        stringBuilder.append(\"<br/>\\n\");\n        stringBuilder.append(\"<br/>\\n\");\n        stringBuilder.append(\"<hr/>\\n\");\n        stringBuilder.append(\"<br/>\\n\");\n        stringBuilder.append(\"<br/>\\n\");\n\n        stringBuilder.append(\n                \"<h2 align=\\\"center\\\">Largest Class Cycle : \" + getClassName(cycle.getCycleName()) + \"</h2>\\n\");\n        stringBuilder.append(\n                \"<h3 align=\\\"center\\\">Limiting number of cycles displayed to 1 to keep page load time fast</h3>\\n\");\n        stringBuilder.append(renderCycleVisuals(cycle));\n\n        stringBuilder.append(\"<div align=\\\"center\\\">\");\n        stringBuilder.append(\"<strong>\");\n        stringBuilder.append(\"Bold text indicates class or relationship to remove to decompose cycle\");\n        stringBuilder.append(\"</strong>\");\n        int classCount = cycle.getCycleNodes().size();\n        int relationshipCount = cycle.getEdgeSet().size();\n        stringBuilder.append(\"<div align=\\\"center\\\">Number of classes: \" + classCount + \"  Number of relationships: \"\n                + relationshipCount + \"<br></div>\");\n        stringBuilder.append(\"</div>\\n\");\n\n        stringBuilder.append(\"<table align=\\\"center\\\" border=\\\"5px\\\">\\n\");\n\n        // Content\n        stringBuilder.append(\"<thead>\\n<tr>\\n\");\n        for (String heading : classCycleTableHeadings) {\n            stringBuilder.append(\"<th>\").append(heading).append(\"</th>\\n\");\n        }\n        stringBuilder.append(\"</thead>\\n\");\n\n        stringBuilder.append(\"<tbody>\\n\");\n\n        for (String vertex : cycle.getVertexSet()) {\n            stringBuilder.append(\"<tr>\");\n            String className = getClassName(vertex);\n            if (vertexesToRemove.contains(vertex)) {\n                className = \"<strong>\" + className + \"</strong>\";\n            }\n\n            stringBuilder.append(drawTableCell(className));\n            StringBuilder edges = new StringBuilder();\n            for (DefaultWeightedEdge edge : cycle.getEdgeSet()) {\n                if (edge.toString().startsWith(\"(\" + vertex + \" :\")) {\n\n                    if (edgesToRemove.contains(edge)) {\n                        edges.append(\"<strong>\");\n                        edges.append(renderEdge(edge));\n                        if (cycle.getMinCutEdges().contains(edge)) {\n                            edges.append(\"*\");\n                        }\n                        edges.append(\"</strong>\");\n                    } else {\n                        edges.append(renderEdge(edge));\n                    }\n\n                    edges.append(\"<br/>\\n\");\n                }\n            }\n            stringBuilder.append(drawTableCell(edges.toString()));\n            stringBuilder.append(\"</tr>\\n\");\n        }\n\n        stringBuilder.append(\"</tbody>\\n\");\n\n        stringBuilder.append(\"</table>\\n\");\n\n        return stringBuilder.toString();\n    }\n\n    public String renderClassGraphVisuals() {\n        return \"\"; // empty on purpose\n    }\n\n    public String renderCycleVisuals(RankedCycle cycle) {\n        return \"\"; // empty on purpose\n    }\n\n    private String renderGodClassInfo(\n            boolean showDetails, List<RankedDisharmony> rankedGodClassDisharmonies, String[] godClassTableHeadings) {\n        int maxGodClassPriority = rankedGodClassDisharmonies\n                .get(rankedGodClassDisharmonies.size() - 1)\n                .getPriority();\n\n        StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(\"<div style=\\\"text-align: center;\\\"><a id=\\\"GOD\\\"><h1>God Classes</h1></a></div>\\n\");\n\n        stringBuilder.append(renderGodClassChart(rankedGodClassDisharmonies, maxGodClassPriority));\n\n        stringBuilder.append(\n                \"<h2 align=\\\"center\\\">God classes by the numbers: (Refactor Starting with Priority 1)</h2>\\n\");\n        stringBuilder.append(\"<table align=\\\"center\\\" border=\\\"5px\\\">\\n\");\n\n        // Content\n        stringBuilder.append(\"<thead><tr>\");\n        for (String heading : godClassTableHeadings) {\n            stringBuilder.append(\"<th>\").append(heading).append(\"</th>\\n\");\n        }\n        stringBuilder.append(\"</tr>\\n</thead>\\n\");\n\n        stringBuilder.append(\"<tbody>\\n\");\n        for (RankedDisharmony rankedGodClassDisharmony : rankedGodClassDisharmonies) {\n            stringBuilder.append(\"<tr>\\n\");\n\n            String[] simpleRankedGodClassDisharmonyData = {\n                rankedGodClassDisharmony.getFileName(),\n                rankedGodClassDisharmony.getPriority().toString(),\n                rankedGodClassDisharmony.getChangePronenessRank().toString(),\n                rankedGodClassDisharmony.getEffortRank().toString(),\n                rankedGodClassDisharmony.getWmc().toString(),\n                formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()),\n                rankedGodClassDisharmony.getCommitCount().toString()\n            };\n\n            String[] detailedRankedGodClassDisharmonyData = {\n                rankedGodClassDisharmony.getFileName(),\n                rankedGodClassDisharmony.getPriority().toString(),\n                rankedGodClassDisharmony.getRawPriority().toString(),\n                rankedGodClassDisharmony.getChangePronenessRank().toString(),\n                rankedGodClassDisharmony.getEffortRank().toString(),\n                rankedGodClassDisharmony.getWmc().toString(),\n                rankedGodClassDisharmony.getWmcRank().toString(),\n                rankedGodClassDisharmony.getAtfd().toString(),\n                rankedGodClassDisharmony.getAtfdRank().toString(),\n                rankedGodClassDisharmony.getTcc().toString(),\n                rankedGodClassDisharmony.getTccRank().toString(),\n                formatter.format(rankedGodClassDisharmony.getFirstCommitTime()),\n                formatter.format(rankedGodClassDisharmony.getMostRecentCommitTime()),\n                rankedGodClassDisharmony.getCommitCount().toString(),\n                rankedGodClassDisharmony.getPath()\n            };\n\n            final String[] rankedDisharmonyData =\n                    showDetails ? detailedRankedGodClassDisharmonyData : simpleRankedGodClassDisharmonyData;\n\n            for (String rowData : rankedDisharmonyData) {\n                stringBuilder.append(drawTableCell(rowData));\n            }\n\n            stringBuilder.append(\"</tr>\\n\");\n        }\n\n        stringBuilder.append(\"</tbody>\\n\");\n        stringBuilder.append(\"</table>\\n\");\n\n        return stringBuilder.toString();\n    }\n\n    private String renderHighlyCoupledClassInfo(List<RankedDisharmony> rankedCBODisharmonies) {\n        StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(\n                \"<div style=\\\"text-align: center;\\\"><a id=\\\"CBO\\\"><h1>Highly Coupled Classes</h1></a></div>\");\n\n        int maxCboPriority =\n                rankedCBODisharmonies.get(rankedCBODisharmonies.size() - 1).getPriority();\n\n        stringBuilder.append(renderCBOChart(rankedCBODisharmonies, maxCboPriority));\n\n        stringBuilder.append(\n                \"<h2 align=\\\"center\\\">Highly Coupled classes by the numbers: (Refactor starting with Priority 1)</h2>\");\n        stringBuilder.append(\"<table align=\\\"center\\\" border=\\\"5px\\\">\");\n\n        // Content\n        stringBuilder.append(\"<thead><tr>\");\n        for (String heading : cboTableHeadings) {\n            stringBuilder.append(\"<th>\").append(heading).append(\"</th>\");\n        }\n        stringBuilder.append(\"</tr></thead>\");\n\n        stringBuilder.append(\"<tbody>\");\n        for (RankedDisharmony rankedCboClassDisharmony : rankedCBODisharmonies) {\n            stringBuilder.append(\"<tr>\");\n\n            String[] rankedCboClassDisharmonyData = {\n                rankedCboClassDisharmony.getFileName(),\n                rankedCboClassDisharmony.getPriority().toString(),\n                rankedCboClassDisharmony.getChangePronenessRank().toString(),\n                rankedCboClassDisharmony.getEffortRank().toString(),\n                formatter.format(rankedCboClassDisharmony.getMostRecentCommitTime()),\n                rankedCboClassDisharmony.getCommitCount().toString()\n            };\n\n            for (String rowData : rankedCboClassDisharmonyData) {\n                stringBuilder.append(drawTableCell(rowData));\n            }\n\n            stringBuilder.append(\"</tr>\");\n        }\n\n        stringBuilder.append(\"</tbody>\");\n        stringBuilder.append(\"</table>\");\n\n        return stringBuilder.toString();\n    }\n\n    String drawTableCell(String rowData) {\n        if (isNumber(rowData) || isDateTime(rowData)) {\n            return new StringBuilder()\n                    .append(\"<td align=\\\"right\\\">\")\n                    .append(rowData)\n                    .append(\"</td>\\n\")\n                    .toString();\n        } else {\n            return new StringBuilder()\n                    .append(\"<td align=\\\"left\\\">\")\n                    .append(rowData)\n                    .append(\"</td>\\n\")\n                    .toString();\n        }\n    }\n\n    boolean isNumber(String rowData) {\n        return rowData.matches(\"-?\\\\d+(\\\\.\\\\d+)?\");\n    }\n\n    boolean isDateTime(String rowData) {\n        return rowData.contains(\", \");\n    }\n\n    public String printTitle(String projectName, String projectVersion) {\n        return \"\"; // empty on purpose\n    }\n\n    public String printHead() {\n        return \"\"; // empty on purpose\n    }\n\n    String printScripts() {\n        return \"\"; // empty on purpose\n    }\n\n    public String printOpenBodyTag() {\n        return \"  <body class=\\\"composite\\\">\\n\";\n    }\n\n    public String printBreadcrumbs() {\n        return \"    <div id=\\\"banner\\\">\\n\"\n                + \"    </div>\\n\"\n                + \"    <div id=\\\"breadcrumbs\\\">\\n\"\n                + \"      <div class=\\\"xleft\\\">\\n\";\n    }\n\n    public String printProjectHeader(String projectName, String projectVersion) {\n        return \"</div>\\n\" + \"      <div class=\\\"xright\\\">      </div>\\n\"\n                + \"    </div>\\n\"\n                + \"    <div id=\\\"bodyColumn\\\">\\n\"\n                + \"      <div id=\\\"contentBox\\\">\\n\" + \"<section>\\n\"\n                + \"<h2><a href=\\\"https://github.com/refactorfirst/refactorfirst\\\" target=\\\"_blank\\\" \"\n                + \"title=\\\"Learn about RefactorFirst\\\" aria-label=\\\"RefactorFirst\\\">RefactorFirst</a> Report for \"\n                + projectName\n                + \" \"\n                + projectVersion\n                + \"</h2>\\n\";\n    }\n\n    public String printProjectFooter() {\n        return \"      <div class=\\\"clear\\\">\\n\" + \"        <hr/>\\n\" + \"      </div>\\n\"\n                + \"<span id=\\\"publishDate\\\">Last Published: \"\n                + formatter.format(Instant.now())\n                + \"      <div class=\\\"clear\\\">\\n\"\n                + \"        <hr/>\\n\" + \"      </div>\\n\" + \"</span>\";\n    }\n\n    String renderGithubButtons() {\n        return \"\"; // empty on purpose\n    }\n\n    String getOutputName() {\n        // This report will generate simple-report.html when invoked in a project with `mvn site`\n        return \"refactor-first-report\";\n    }\n\n    String renderGodClassChart(List<RankedDisharmony> rankedGodClassDisharmonies, int maxGodClassPriority) {\n        return \"\"; // empty on purpose\n    }\n\n    String writeGodClassGchartJs(List<RankedDisharmony> rankedDisharmonies, int maxPriority) {\n        // return empty string on purpose\n        return \"\";\n    }\n\n    String writeGCBOGchartJs(List<RankedDisharmony> rankedDisharmonies, int maxPriority) {\n        // return empty string on purpose\n        return \"\";\n    }\n\n    String renderCBOChart(List<RankedDisharmony> rankedCBODisharmonies, int maxCboPriority) {\n        return \"\"; // empty on purpose\n    }\n\n    String getClassName(String fqn) {\n        // handle no package\n        if (!fqn.contains(\".\")) {\n            return fqn;\n        }\n\n        int lastIndex = fqn.lastIndexOf(\".\");\n        return fqn.substring(lastIndex + 1);\n    }\n\n    static String[] extractVertexes(DefaultWeightedEdge edge) {\n        return edge.toString().replace(\"(\", \"\").replace(\")\", \"\").split(\":\");\n    }\n}\n"
  },
  {
    "path": "report/src/main/java/org/hjug/refactorfirst/report/json/JsonReport.java",
    "content": "package org.hjug.refactorfirst.report.json;\n\nimport java.util.List;\nimport lombok.Builder;\nimport lombok.Data;\n\n@Data\n@Builder\nclass JsonReport {\n    private List<JsonReportDisharmonyEntry> rankedDisharmonies;\n\n    private List<String> errors;\n}\n"
  },
  {
    "path": "report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportDisharmonyEntry.java",
    "content": "package org.hjug.refactorfirst.report.json;\n\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.FormatStyle;\nimport java.util.Locale;\nimport lombok.Builder;\nimport lombok.Data;\nimport org.hjug.cbc.RankedDisharmony;\n\n@Data\n@Builder\nclass JsonReportDisharmonyEntry {\n    private static final DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)\n            .withLocale(Locale.getDefault())\n            .withZone(ZoneId.systemDefault());\n\n    private final String fileName;\n\n    private final String className;\n\n    private final String fullFilePath;\n\n    private final Integer effortRank;\n\n    private final Integer changePronenessRank;\n\n    private final Integer priority;\n\n    private final Integer weightedMethodCount;\n\n    private final Integer commitCount;\n\n    private final String mostRecentCommitTime;\n\n    public static JsonReportDisharmonyEntry fromRankedDisharmony(RankedDisharmony entry) {\n        return JsonReportDisharmonyEntry.builder()\n                .fileName(entry.getFileName())\n                .className(entry.getClassName())\n                .effortRank(entry.getEffortRank())\n                .changePronenessRank(entry.getChangePronenessRank())\n                .priority(entry.getRawPriority())\n                .weightedMethodCount(entry.getWmc())\n                .commitCount(entry.getCommitCount())\n                .mostRecentCommitTime(formatter.format(entry.getMostRecentCommitTime()))\n                .fullFilePath(entry.getPath())\n                .build();\n    }\n}\n"
  },
  {
    "path": "report/src/main/java/org/hjug/refactorfirst/report/json/JsonReportExecutor.java",
    "content": "package org.hjug.refactorfirst.report.json;\n\nimport static org.hjug.refactorfirst.report.ReportWriter.writeReportToDisk;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.hjug.cbc.CostBenefitCalculator;\nimport org.hjug.cbc.RankedDisharmony;\n\n@Slf4j\npublic class JsonReportExecutor {\n\n    private static final String FILE_NAME = \"refactor-first-data.json\";\n\n    private static final ObjectMapper MAPPER = new ObjectMapper();\n\n    public void execute(File baseDir, String outputDirectory) {\n        String projectBaseDir;\n\n        if (baseDir != null) {\n            projectBaseDir = baseDir.getPath();\n        } else {\n            projectBaseDir = Paths.get(\"\").toAbsolutePath().toString();\n        }\n\n        // TODO: revisit\n        final CostBenefitCalculator costBenefitCalculator = new CostBenefitCalculator(projectBaseDir, new HashMap<>());\n        try {\n            costBenefitCalculator.runPmdAnalysis();\n        } catch (IOException e) {\n            log.error(\"Error running PMD analysis.\");\n            throw new RuntimeException(e);\n        }\n        final List<RankedDisharmony> rankedDisharmonies = costBenefitCalculator.calculateGodClassCostBenefitValues();\n        final List<JsonReportDisharmonyEntry> disharmonyEntries = rankedDisharmonies.stream()\n                .map(JsonReportDisharmonyEntry::fromRankedDisharmony)\n                .collect(Collectors.toList());\n\n        final JsonReport report =\n                JsonReport.builder().rankedDisharmonies(disharmonyEntries).build();\n\n        try {\n            final String reportJson = MAPPER.writeValueAsString(report);\n\n            writeReportToDisk(outputDirectory, FILE_NAME, new StringBuilder(reportJson).toString());\n        } catch (final JsonProcessingException jsonProcessingException) {\n            final String errorMessage = \"Could not generate a json report: \" + jsonProcessingException;\n\n            log.error(errorMessage);\n            final JsonReport errorReport = JsonReport.builder()\n                    .errors(new ArrayList<>(Collections.singletonList(errorMessage)))\n                    .build();\n\n            writeErrorReport(errorReport, outputDirectory);\n        }\n    }\n\n    private void writeErrorReport(final JsonReport errorReport, String outputDirectory) {\n        try {\n            writeReportToDisk(\n                    outputDirectory, FILE_NAME, new StringBuilder(MAPPER.writeValueAsString(errorReport)).toString());\n        } catch (final JsonProcessingException jsonProcessingException) {\n            log.error(\"failed to write error report: \", jsonProcessingException);\n        }\n    }\n}\n"
  },
  {
    "path": "report/src/test/java/org/hjug/refactorfirst/report/HtmlReportTest.java",
    "content": "package org.hjug.refactorfirst.report;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport java.util.*;\nimport org.hjug.cbc.CycleNode;\nimport org.hjug.cbc.RankedCycle;\nimport org.jgrapht.Graph;\nimport org.jgrapht.graph.DefaultDirectedWeightedGraph;\nimport org.jgrapht.graph.DefaultWeightedEdge;\nimport org.junit.jupiter.api.Test;\n\nclass HtmlReportTest {\n\n    private HtmlReport mavenReport = new HtmlReport();\n\n    @Test\n    void testGetOutputName() {\n        // This report will generate simple-report.html when invoked in a project with `mvn site`\n        assertEquals(\"refactor-first-report\", mavenReport.getOutputName());\n    }\n\n    @Test\n    void getName() {\n        // Name of the report when listed in the project-reports.html page of a project\n        assertEquals(\"Refactor First Report\", mavenReport.getName(Locale.getDefault()));\n    }\n\n    @Test\n    void getDescription() {\n        // Description of the report when listed in the project-reports.html page of a project\n        assertEquals(\n                \"Ranks the disharmonies in a codebase.  The classes that should be refactored first \"\n                        + \" have the highest priority values.\",\n                mavenReport.getDescription(Locale.getDefault()));\n    }\n\n    @Test\n    void buildCycleDot() {\n        Graph<String, DefaultWeightedEdge> classGraph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);\n        classGraph.addVertex(\"A\");\n        classGraph.addVertex(\"B\");\n        classGraph.addVertex(\"C\");\n        classGraph.addEdge(\"A\", \"B\");\n        classGraph.addEdge(\"B\", \"C\");\n        classGraph.addEdge(\"C\", \"A\");\n        classGraph.setEdgeWeight(\"A\", \"B\", 2);\n\n        String cycleName = \"Test\";\n        List<CycleNode> cycleNodes = new ArrayList<>();\n        RankedCycle rankedCycle =\n                new RankedCycle(cycleName, 0, classGraph.vertexSet(), classGraph.edgeSet(), 0, null, cycleNodes);\n\n        HtmlReport htmlReport = new HtmlReport();\n        String dot = htmlReport.buildCycleDot(classGraph, rankedCycle);\n\n        String expectedDot = \"`strict digraph G {\\n\"\n                + \"A -> B [ label = \\\"2\\\" weight = \\\"2\\\" ];\\n\"\n                + \"B -> C [ label = \\\"1\\\" weight = \\\"1\\\" ];\\n\"\n                + \"C -> A [ label = \\\"1\\\" weight = \\\"1\\\" ];\\n\"\n                + \"A;\\n\"\n                + \"B;\\n\"\n                + \"C;\\n\"\n                + \"}`;\";\n\n        assertEquals(expectedDot, dot);\n    }\n}\n"
  },
  {
    "path": "report/src/test/java/org/hjug/refactorfirst/report/SimpleHtmlReportTest.java",
    "content": "package org.hjug.refactorfirst.report;\n\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nclass SimpleHtmlReportTest {\n\n    @Test\n    void isDateTime() {\n        HtmlReport htmlReport = new HtmlReport();\n        String commitDateTime = \"7/22/23, 5:00 AM\";\n        Assertions.assertTrue(htmlReport.isDateTime(commitDateTime));\n    }\n}\n"
  },
  {
    "path": "report/src/test/resources/highlight.html",
    "content": "<head>\n    <style> body { margin: 0; } </style>\n\n    <script src=\"https://unpkg.com/3d-force-graph\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/graphlib-dot@0.6.4/dist/graphlib-dot.min.js\"></script>\n</head>\n\n<body>\n<div id=\"3d-graph\"></div>\n\n<script>\n    const dot = `\n            digraph {\n                node [style=filled];\n                A [color=red];\n                B [color=green];\n                C [color=blue];\n                A -> B [color=purple];\n                B -> C [color=orange];\n                C -> A [color=cyan];\n            }`;\n\n    const graphlibGraph = graphlibDot.read(dot);\n\n    var allNodes = [];\n    var allLinks = [];\n\n    graphlibGraph.nodes().forEach(function(node) {\n        var nodeData = graphlibGraph.node(node);\n        allNodes.push({\n            id: node,\n            color: nodeData.color || 'white',\n        });\n    });\n\n    graphlibGraph.edges().forEach(function(edge) {\n        allLinks.push({\n            source: edge.v,\n            target: edge.w,\n            color: graphlibGraph.edge(edge).color || 'white'\n        });\n    });\n\n    const gData = {\n        nodes: allNodes,\n        links: allLinks\n    };\n\n    // cross-link node objects\n    gData.links.forEach(link => {\n        const a = gData.nodes.find(node => node.id === link.source);\n        const b = gData.nodes.find(node => node.id === link.target);\n        !a.neighbors && (a.neighbors = []);\n        !b.neighbors && (b.neighbors = []);\n        a.neighbors.push(b);\n        b.neighbors.push(a);\n\n        !a.links && (a.links = []);\n        !b.links && (b.links = []);\n        a.links.push(link);\n        b.links.push(link);\n    });\n\n    const highlightNodes = new Set();\n    const highlightLinks = new Set();\n    let hoverNode = null;\n\n    const Graph = new ForceGraph3D(document.getElementById('3d-graph'))\n        .graphData(gData)\n        .nodeLabel('id')\n        .nodeColor(node => highlightNodes.has(node) ? node === hoverNode ? 'rgb(255,0,0,1)' : 'rgba(255,160,0,0.8)' : 'rgba(0,255,255,0.6)')\n        .linkWidth(link => highlightLinks.has(link) ? 4 : 1)\n        .linkDirectionalParticles(link => highlightLinks.has(link) ? 4 : 0)\n        .linkDirectionalParticleWidth(4)\n\n        .onNodeHover(node => {\n            // no state change\n            if ((!node && !highlightNodes.size) || (node && hoverNode === node)) return;\n\n            highlightNodes.clear();\n            highlightLinks.clear();\n            if (node) {\n                highlightNodes.add(node);\n                node.neighbors.forEach(neighbor => highlightNodes.add(neighbor));\n                node.links.forEach(link => highlightLinks.add(link));\n            }\n\n            hoverNode = node || null;\n\n            updateHighlight(Graph);\n        })\n        .onLinkHover(link => {\n            highlightNodes.clear();\n            highlightLinks.clear();\n\n            if (link) {\n                highlightLinks.add(link);\n                highlightNodes.add(link.source);\n                highlightNodes.add(link.target);\n            }\n\n            updateHighlight(Graph);\n        });\n\n    function updateHighlight(Graph) {\n        // trigger update of highlighted objects in scene\n        Graph\n            .nodeColor(Graph.nodeColor())\n            .linkWidth(Graph.linkWidth())\n            .linkDirectionalParticles(Graph.linkDirectionalParticles());\n    }\n</script>\n</body>"
  },
  {
    "path": "report/src/test/resources/sigmaPlayground.html",
    "content": "<!-- This is a playground for experimentation -->\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>DOT Graph with Sigma.js and Graphology</title>\n    <script async defer src=\"https://buttons.github.io/buttons.js\"></script>\n    <script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>\n    <script src=\"https://d3js.org/d3.v5.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/d3-graphviz/3.0.5/d3-graphviz.min.js\"></script>\n    <script src=\"https://unpkg.com/@hpcc-js/wasm@0.3.11/dist/index.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/graphlib-dot@0.6.4/dist/graphlib-dot.min.js\"></script>\n    <script src=\"https://unpkg.com/3d-force-graph\"></script>\n    <style>\n        /* Popup container */\n        .popup {\n            position: fixed;\n            display: none;\n            width: 95%;\n            height: 95%;\n            background-color: white;\n            border: 1px solid #ccc;\n            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            z-index: 1000;\n            padding: 20px;\n            box-sizing: border-box;\n        }\n\n        /* Popup overlay */\n        .overlay {\n            position: fixed;\n            display: none;\n            width: 100%;\n            height: 100%;\n            top: 0;\n            left: 0;\n            background: rgba(0, 0, 0, 0.5);\n            z-index: 999;\n        }\n\n        /* Close button */\n        .close-btn {\n            position: absolute;\n            top: 10px;\n            right: 10px;\n            cursor: pointer;\n        }\n    </style>\n    <script>\n        function sugiyamaLayout(graph) {\n            var layers = [];\n            var nodeLevels = {};\n            var nodes = graph.nodes();\n            //var edges = graph.edges();\n\n            // Step 1: Assign levels to nodes\n            function assignLevels() {\n                var visited = {};\n                var stack = [];\n\n                function visit(node, level) {\n                    if (visited[node]) return;\n                    visited[node] = true;\n                    nodeLevels[node] = level;\n                    if (!layers[level]) layers[level] = [];\n                    layers[level].push(node);\n                    stack.push(node);\n                    graph.forEachNeighbor(node, function (neighbor) {\n                        visit(neighbor, level + 1);\n                    });\n                }\n\n                nodes.forEach(function (node) {\n                    if (!visited[node]) visit(node, 0);\n                });\n            }\n\n            // Step 2: Reduce edge crossings\n            function reduceCrossings() {\n                for (var i = 0; i < layers.length - 1; i++) {\n                    var layer = layers[i];\n                    var nextLayer = layers[i + 1];\n                    var positions = {};\n\n                    nextLayer.forEach(function (node, index) {\n                        positions[node] = index;\n                    });\n\n                    layer.sort(function (a, b) {\n                        var aPos = 0, bPos = 0;\n                        graph.forEachNeighbor(a, function (neighbor) {\n                            aPos += positions[neighbor] || 0;\n                        });\n                        graph.forEachNeighbor(b, function (neighbor) {\n                            bPos += positions[neighbor] || 0;\n                        });\n                        return aPos - bPos;\n                    });\n                }\n            }\n\n            // Step 3: Assign positions to nodes\n            function assignPositions() {\n                var yStep = 100;\n                var xStep = 2000;\n\n                layers.forEach(function (layer, level) {\n                    var layerWidth = layer.length * xStep;\n                    var offsetX = ((screen.width - 200) - layerWidth) / 2; // Centering the nodes\n\n                    layer.forEach(function (node, index) {\n                        graph.setNodeAttribute(node, 'x', offsetX + index * xStep);\n                        graph.setNodeAttribute(node, 'y', -level * yStep);\n                    });\n                });\n            }\n\n            assignLevels();\n            reduceCrossings();\n            assignPositions();\n        }\n\n        function renderGraph(dot) {\n            // Parse the DOT graph using graphlib-dot\n            const graphlibGraph = graphlibDot.read(dot);\n\n            // Convert graphlib graph to graphology graph\n            const graphologyGraph = new graphology.Graph();\n\n            /*\n            Node Attributes\n            id: A unique identifier for the node (usually a string).\n            label: A label or name for the node.\n            color: The color of the node (e.g., \"red\", \"#ff0000\").\n            style: The style of the node (e.g., \"filled\", \"dotted\").\n            shape: The shape of the node (e.g., \"circle\", \"box\").\n            width: The width of the node.\n            height: The height of the node.\n            */\n            graphlibGraph.nodes().forEach(node => {\n                const attrs = graphlibGraph.node(node);\n                graphologyGraph.addNode(node, {\n                    label: node,\n                    color: attrs.color,\n                    // x: Math.random(),\n                    // y: Math.random(),\n                    size: 5,\n                });\n            });\n\n            /*\n            Edge Attributes\n            v: The id of the source node.\n            w: The id of the target node.\n            label: A label or name for the edge.\n            color: The color of the edge (e.g., \"blue\", \"#0000ff\").\n            style: The style of the edge (e.g., \"solid\", \"dashed\").\n            weight: The weight of the edge.\n            arrowhead: The style of the arrowhead (e.g., \"normal\", \"vee\").\n             */\n            graphlibGraph.edges().forEach(edge => {\n                const attrs = graphlibGraph.edge(edge);\n                graphologyGraph.addEdge(edge.v, edge.w, {\n                    color: attrs.color,\n                    size: 1,\n                    type: 'arrow',\n                });\n            });\n\n            sugiyamaLayout(graphologyGraph)\n\n            return graphologyGraph;\n        }\n    </script>\n    <script type=\"module\">\n        // SpriteText will only work as import\n        // this script block requires type=module since we are using an import\n        import SpriteText from \"https://esm.sh/three-spritetext\";\n\n        function createForceGraph(popupId, containerName, dot) {\n            // Add event listener for Escape key to close the popup\n            document.addEventListener('keydown', function (event) {\n                if (event.key === 'Escape') {\n                    hidePopup();\n                }\n            });\n\n            document.getElementById('overlay').style.display = 'block';\n            document.getElementById(popupId).style.display = 'block';\n            var container = document.getElementById(containerName);\n\n            // Parse the DOT graph using graphlib-dot\n            const graphlibGraph = graphlibDot.read(dot);\n\n            var nodes = [];\n            var links = [];\n\n            graphlibGraph.nodes().forEach(function (node) {\n                var nodeData = graphlibGraph.node(node);\n                nodes.push({\n                    id: node,\n                    color: nodeData.color || 'white',\n                });\n            });\n\n            graphlibGraph.edges().forEach(function (edge) {\n                links.push({\n                    source: edge.v,\n                    target: edge.w,\n                    color: graphlibGraph.edge(edge).color || 'white',\n                    weight: graphlibGraph.edge(edge).weight,\n                });\n            });\n\n            const gData = {\n                nodes: nodes,\n                links: links\n            };\n\n            // cross-link node objects\n            gData.links.forEach(link => {\n                const a = gData.nodes.find(node => node.id === link.source);\n                const b = gData.nodes.find(node => node.id === link.target);\n                !a.neighbors && (a.neighbors = []);\n                !b.neighbors && (b.neighbors = []);\n                a.neighbors.push(b);\n                b.neighbors.push(a);\n\n                !a.links && (a.links = []);\n                !b.links && (b.links = []);\n                a.links.push(link);\n                b.links.push(link);\n            });\n\n            const Graph = new ForceGraph3D(container)\n                .graphData(gData)\n                .nodeLabel('id')\n                .width(container.clientWidth)\n                .height(container.clientHeight);\n\n            if(gData.links.length + gData.nodes.length < 4000) {\n                console.log(gData.links.length + gData.nodes.length);\n\n\n                // use node labels instead of spheres\n                Graph.nodeThreeObject(node => {\n                    const sprite = new SpriteText(node.id);\n                    sprite.material.depthWrite = false; // make sprite background transparent\n                    sprite.color = node.color;\n                    sprite.textHeight = 4;\n                    return sprite;\n                });\n\n                // code to display weight as link text\n                // may be too much for browsers to handle\n                // Graph\n                //     .linkThreeObjectExtend(true)\n                //     .linkThreeObject(link => {\n                //         // extend link with text sprite\n                //         const sprite = new SpriteText(`${link.weight}`);\n                //         sprite.color = 'lightgrey';\n                //         sprite.textHeight = 3;\n                //         return sprite;\n                //     })\n                //     .linkPositionUpdate((sprite, {start, end}) => {\n                //         const middlePos = Object.assign(...['x', 'y', 'z'].map(c => ({\n                //             [c]: start[c] + (end[c] - start[c]) / 2 // calc middle point\n                //         })));\n                //\n                //         // Position sprite\n                //         Object.assign(sprite.position, middlePos);\n                //     });\n\n\n                // code to highlight nodes & links\n                // TODO: enable via control - see Manipulate Link Force Distance for example\n                const highlightNodes = new Set();\n                const highlightLinks = new Set();\n                let hoverNode = null;\n                Graph\n                    .nodeColor(node => highlightNodes.has(node) ? node === hoverNode ? 'rgb(255,0,0,1)' : 'rgba(255,160,0,0.8)' : 'rgba(0,255,255,0.6)')\n                    .linkWidth(link => highlightLinks.has(link) ? 4 : 1)\n                    .linkDirectionalParticles(link => highlightLinks.has(link) ? 4 : 0)\n                    .linkDirectionalParticleWidth(4)\n                    .onNodeHover(node => {\n                        // no state change\n                        if ((!node && !highlightNodes.size) || (node && hoverNode === node)) return;\n\n                        highlightNodes.clear();\n                        highlightLinks.clear();\n                        if (node) {\n                            highlightNodes.add(node);\n                            node.neighbors.forEach(neighbor => highlightNodes.add(neighbor));\n                            node.links.forEach(link => highlightLinks.add(link));\n                        }\n\n                        hoverNode = node || null;\n\n                        updateHighlight(Graph);\n                    })\n                    .onLinkHover(link => {\n                        highlightNodes.clear();\n                        highlightLinks.clear();\n\n                        if (link) {\n                            highlightLinks.add(link);\n                            highlightNodes.add(link.source);\n                            highlightNodes.add(link.target);\n                        }\n\n                        updateHighlight(Graph);\n                    });\n\n            }\n        }\n\n        // used by highlighting functionality\n        function updateHighlight(Graph) {\n            // trigger update of highlighted objects in scene\n            Graph\n                .nodeColor(Graph.nodeColor())\n                .linkWidth(Graph.linkWidth())\n                .linkDirectionalParticles(Graph.linkDirectionalParticles());\n        }\n\n        // needed to allow the button to open the graph\n        window.createForceGraph = createForceGraph;\n    </script>\n\n    <script>\n        function showPopup(popupId, containerName, dot) {\n            // Add event listener for Escape key to close the popup\n            document.addEventListener('keydown', function (event) {\n                if (event.key === 'Escape') {\n                    hidePopup();\n                }\n            });\n\n            document.getElementById('overlay').style.display = 'block';\n            document.getElementById(popupId).style.display = 'block';\n\n            var graph = renderGraph(dot);\n            var container = document.getElementById(containerName);\n\n            // Render with Sigma.js\n            new Sigma(graph, container);\n        }\n\n        function hidePopup() {\n            document.getElementById('overlay').style.display = 'none';\n            var popups = document.getElementsByClassName('popup');\n            for (var i = 0; i < popups.length; i++) {\n                popups[i].style.display = 'none';\n            }\n\n            // Clear the graph containers to remove the previous graphs\n            var containers = document.querySelectorAll('[id^=\"graph-container\"]');\n            containers.forEach(function (container) {\n                while (container.firstChild) {\n                    container.removeChild(container.firstChild);\n                }\n            });\n\n            // Remove the Escape key event listener\n            document.removeEventListener('keydown', function (event) {\n                if (event.key === 'Escape') {\n                    hidePopup();\n                }\n            });\n        }\n    </script>\n</head>\n\n<!--\ncreate a fully implemented html page that renders a DOT graph with sigma.js, graphlib, graphlib-dot, and graphology, creates the nodes with graphology to capture the node and edge colors specified in the DOT graph, and without using require.js and only using client-side javascript and html\n-->\n<body>\n<div class=\"overlay\" id=\"overlay\" onclick=\"hidePopup()\"></div>\n\n<script>\n    const dot = `\n            digraph {\n                node [style=filled];\n                A [color=red];\n                B [color=green];\n                C [color=blue];\n                A -> B [color=purple];\n                B -> C [color=orange];\n                C -> A [color=cyan];\n            }\n        `;\n    const mb_dot = `strict digraph G {\nAboutDialog -> MiscTools [ label = \"10\" weight = \"10\" color = \"red\" ];\nAboutDialog -> MainPanelView [ label = \"1\" weight = \"1\" color = \"red\" ];\nChunkDownloader -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nChunkDownloader -> Download [ label = \"4\" weight = \"4\" color = \"red\" ];\nChunkDownloader -> ChunkWriterManager [ label = \"2\" weight = \"2\" ];\nChunkDownloader -> SmartMegaProxyManager [ label = \"4\" weight = \"4\" color = \"red\" ];\nChunkDownloader -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nChunkDownloader -> ThrottledInputStream [ label = \"1\" weight = \"1\" ];\nChunkDownloader -> ChunkInvalidException [ label = \"1\" weight = \"1\" ];\nChunkDownloaderMono -> ChunkDownloader [ label = \"2\" weight = \"2\" ];\nChunkDownloaderMono -> Download [ label = \"3\" weight = \"3\" color = \"red\" ];\nChunkDownloaderMono -> CryptTools [ label = \"2\" weight = \"2\" color = \"red\" ];\nChunkDownloaderMono -> ChunkInvalidException [ label = \"1\" weight = \"1\" ];\nChunkUploader -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nChunkUploader -> Upload [ label = \"5\" weight = \"5\" color = \"red\" ];\nChunkUploader -> UploadMACGenerator [ label = \"1\" weight = \"1\" ];\nChunkUploader -> ChunkWriterManager [ label = \"1\" weight = \"1\" ];\nChunkUploader -> ThrottledOutputStream [ label = \"1\" weight = \"1\" ];\nChunkUploader -> ChunkInvalidException [ label = \"1\" weight = \"1\" ];\nChunkWriterManager -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nChunkWriterManager -> Download [ label = \"3\" weight = \"3\" color = \"red\" ];\nChunkWriterManager -> ChunkInvalidException [ label = \"1\" weight = \"1\" ];\nClipboardChangeObservable -> ClipboardChangeObserver [ label = \"2\" weight = \"2\" ];\nClipboardSpy -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nClipboardSpy -> ClipboardChangeObservable [ label = \"1\" weight = \"1\" ];\nClipboardSpy -> ClipboardChangeObserver [ label = \"3\" weight = \"3\" ];\nClipboardSpy -> DBTools [ label = \"1\" weight = \"1\" ];\nContextMenuMouseListener -> ContextMenuMouseListener__Actions [ label = \"1\" weight = \"1\" ];\nCryptTools -> MiscTools [ label = \"14\" weight = \"14\" ];\nCryptTools -> MainPanel [ label = \"2\" weight = \"2\" ];\nCryptTools -> GetMasterPasswordDialog [ label = \"2\" weight = \"2\" ];\nDBTools -> SqliteSingleton [ label = \"36\" weight = \"36\" ];\nDownload -> Transference [ label = \"1\" weight = \"1\" ];\nDownload -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nDownload -> TransferenceManager [ label = \"8\" weight = \"8\" ];\nDownload -> MiscTools [ label = \"7\" weight = \"7\" color = \"red\" ];\nDownload -> DownloadView [ label = \"6\" weight = \"6\" ];\nDownload -> MainPanel [ label = \"3\" weight = \"3\" color = \"red\" ];\nDownload -> ProgressMeter [ label = \"2\" weight = \"2\" ];\nDownload -> ChunkDownloader [ label = \"10\" weight = \"10\" ];\nDownload -> ChunkWriterManager [ label = \"2\" weight = \"2\" ];\nDownload -> MegaAPI [ label = \"3\" weight = \"3\" ];\nDownload -> ChunkDownloaderMono [ label = \"2\" weight = \"2\" ];\nDownload -> DBTools [ label = \"2\" weight = \"2\" ];\nDownload -> APIException [ label = \"5\" weight = \"5\" ];\nDownload -> CryptTools [ label = \"2\" weight = \"2\" color = \"red\" ];\nDownload -> ChunkInvalidException [ label = \"2\" weight = \"2\" ];\nDownload -> MainPanelView [ label = \"1\" weight = \"1\" color = \"red\" ];\nDownloadManager -> TransferenceManager [ label = \"4\" weight = \"4\" ];\nDownloadManager -> MiscTools [ label = \"2\" weight = \"2\" color = \"red\" ];\nDownloadManager -> Download [ label = \"2\" weight = \"2\" ];\nDownloadManager -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nDownloadManager -> Transference [ label = \"8\" weight = \"8\" ];\nDownloadManager -> APIException [ label = \"3\" weight = \"3\" ];\nDownloadView -> TransferenceView [ label = \"1\" weight = \"1\" ];\nDownloadView -> MiscTools [ label = \"12\" weight = \"12\" color = \"red\" ];\nDownloadView -> Download [ label = \"6\" weight = \"6\" color = \"red\" ];\nFileGrabberDialog -> MiscTools [ label = \"8\" weight = \"8\" color = \"red\" ];\nFileGrabberDialog -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nFileGrabberDialog -> MainPanelView [ label = \"1\" weight = \"1\" color = \"red\" ];\nFileGrabberDialog -> DBTools [ label = \"1\" weight = \"1\" ];\nFileGrabberDialog -> MegaAPI [ label = \"2\" weight = \"2\" ];\nFileMergerDialog -> MiscTools [ label = \"3\" weight = \"3\" color = \"red\" ];\nFileMergerDialog -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nFileMergerDialog -> MainPanelView [ label = \"1\" weight = \"1\" color = \"red\" ];\nFileSplitterDialog -> MiscTools [ label = \"4\" weight = \"4\" color = \"red\" ];\nFileSplitterDialog -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nFileSplitterDialog -> MainPanelView [ label = \"1\" weight = \"1\" color = \"red\" ];\nFolderLinkDialog -> MiscTools [ label = \"5\" weight = \"5\" color = \"red\" ];\nFolderLinkDialog -> MainPanelView [ label = \"1\" weight = \"1\" color = \"red\" ];\nFolderLinkDialog -> MegaAPI [ label = \"2\" weight = \"2\" ];\nFolderLinkDialog -> MegaMutableTreeNode [ label = \"6\" weight = \"6\" ];\nFolderLinkDialog -> MegaAPIException [ label = \"1\" weight = \"1\" ];\nGet2FACode -> MiscTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nGet2FACode -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nGetMasterPasswordDialog -> MiscTools [ label = \"2\" weight = \"2\" color = \"red\" ];\nGetMasterPasswordDialog -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nGetMasterPasswordDialog -> CryptTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nKissVideoStreamServer -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nKissVideoStreamServer -> MainPanelView [ label = \"3\" weight = \"3\" ];\nKissVideoStreamServer -> MainPanel [ label = \"3\" weight = \"3\" color = \"red\" ];\nKissVideoStreamServer -> ContentType [ label = \"2\" weight = \"2\" ];\nKissVideoStreamServer -> MegaAPI [ label = \"4\" weight = \"4\" ];\nKissVideoStreamServer -> APIException [ label = \"2\" weight = \"2\" ];\nKissVideoStreamServer -> StreamChunkManager [ label = \"1\" weight = \"1\" ];\nKissVideoStreamServer -> StreamChunkDownloader [ label = \"4\" weight = \"4\" ];\nKissVideoStreamServer -> CryptTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nLabelTranslatorSingleton_LazyHolder -> LabelTranslatorSingleton [ label = \"2\" weight = \"2\" ];\nLinkGrabberDialog -> ClipboardChangeObserver [ label = \"1\" weight = \"1\" ];\nLinkGrabberDialog -> MiscTools [ label = \"4\" weight = \"4\" color = \"red\" ];\nLinkGrabberDialog -> ClipboardSpy [ label = \"2\" weight = \"2\" ];\nLinkGrabberDialog -> MainPanel [ label = \"3\" weight = \"3\" color = \"red\" ];\nLinkGrabberDialog -> MainPanelView [ label = \"1\" weight = \"1\" color = \"red\" ];\nLinkGrabberDialog -> CryptTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nMainPanel -> MiscTools [ label = \"9\" weight = \"9\" color = \"red\" ];\nMainPanel -> SmartMegaProxyManager [ label = \"3\" weight = \"3\" color = \"red\" ];\nMainPanel -> MainPanelView [ label = \"2\" weight = \"2\" ];\nMainPanel -> SpeedMeter [ label = \"3\" weight = \"3\" ];\nMainPanel -> DownloadManager [ label = \"2\" weight = \"2\" ];\nMainPanel -> UploadManager [ label = \"2\" weight = \"2\" ];\nMainPanel -> StreamThrottlerSupervisor [ label = \"2\" weight = \"2\" ];\nMainPanel -> MegaAPI [ label = \"8\" weight = \"8\" ];\nMainPanel -> ClipboardSpy [ label = \"2\" weight = \"2\" ];\nMainPanel -> KissVideoStreamServer [ label = \"2\" weight = \"2\" ];\nMainPanel -> MegaProxyServer [ label = \"3\" weight = \"3\" ];\nMainPanel -> DBTools [ label = \"25\" weight = \"25\" ];\nMainPanel -> Transference [ label = \"6\" weight = \"6\" ];\nMainPanel -> Download [ label = \"5\" weight = \"5\" ];\nMainPanel -> Upload [ label = \"5\" weight = \"5\" ];\nMainPanel -> WarningExitMessage [ label = \"2\" weight = \"2\" ];\nMainPanelView -> MiscTools [ label = \"7\" weight = \"7\" color = \"red\" ];\nMainPanelView -> ClipboardSpy [ label = \"4\" weight = \"4\" ];\nMainPanelView -> MainPanel [ label = \"4\" weight = \"4\" color = \"red\" ];\nMainPanelView -> DownloadManager [ label = \"2\" weight = \"2\" ];\nMainPanelView -> TransferenceManager [ label = \"1\" weight = \"1\" ];\nMainPanelView -> FileGrabberDialog [ label = \"6\" weight = \"6\" ];\nMainPanelView -> MegaAPI [ label = \"7\" weight = \"7\" ];\nMainPanelView -> MegaDirNode [ label = \"4\" weight = \"4\" ];\nMainPanelView -> Upload [ label = \"2\" weight = \"2\" ];\nMainPanelView -> DBTools [ label = \"3\" weight = \"3\" ];\nMainPanelView -> LinkGrabberDialog [ label = \"3\" weight = \"3\" ];\nMainPanelView -> Download [ label = \"1\" weight = \"1\" ];\nMainPanelView -> FolderLinkDialog [ label = \"3\" weight = \"3\" ];\nMainPanelView -> SettingsDialog [ label = \"2\" weight = \"2\" ];\nMainPanelView -> AboutDialog [ label = \"2\" weight = \"2\" ];\nMainPanelView -> StreamerDialog [ label = \"2\" weight = \"2\" ];\nMainPanelView -> FileSplitterDialog [ label = \"2\" weight = \"2\" ];\nMainPanelView -> FileMergerDialog [ label = \"2\" weight = \"2\" ];\nMegaAPI -> CryptTools [ label = \"4\" weight = \"4\" color = \"red\" ];\nMegaAPI -> MiscTools [ label = \"20\" weight = \"20\" color = \"red\" ];\nMegaAPI -> SmartMegaProxyManager [ label = \"4\" weight = \"4\" color = \"red\" ];\nMegaAPI -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nMegaAPI -> MegaAPIException [ label = \"9\" weight = \"9\" ];\nMegaAPI -> Upload [ label = \"1\" weight = \"1\" ];\nMegaAPI -> ThrottledOutputStream [ label = \"1\" weight = \"1\" ];\nMegaAPIException -> APIException [ label = \"2\" weight = \"2\" ];\nMegaCrypterAPI -> CryptTools [ label = \"1\" weight = \"1\" ];\nMegaCrypterAPI -> MainPanelView [ label = \"1\" weight = \"1\" ];\nMegaCrypterAPI -> MiscTools [ label = \"5\" weight = \"5\" ];\nMegaCrypterAPI -> MegaCrypterAPIException [ label = \"3\" weight = \"3\" ];\nMegaCrypterAPIException -> APIException [ label = \"2\" weight = \"2\" ];\nMegaMutableTreeNode -> MegaMutableTreeNode_1 [ label = \"1\" weight = \"1\" ];\nMegaProxyServer -> MainPanelView [ label = \"2\" weight = \"2\" ];\nMegaProxyServer -> MainPanel [ label = \"2\" weight = \"2\" color = \"red\" ];\nMegaProxyServer_Handler -> MegaProxyServer_Handler_1 [ label = \"1\" weight = \"1\" ];\nMiscTools -> MegaMutableTreeNode [ label = \"4\" weight = \"4\" ];\nMiscTools -> SmartMegaProxyManager [ label = \"4\" weight = \"4\" ];\nMiscTools -> MainPanel [ label = \"2\" weight = \"2\" ];\nMiscTools -> MegaAPI [ label = \"4\" weight = \"4\" ];\nMiscTools -> GetMasterPasswordDialog [ label = \"2\" weight = \"2\" ];\nMiscTools -> DBTools [ label = \"1\" weight = \"1\" ];\nMiscTools -> Get2FACode [ label = \"2\" weight = \"2\" ];\nMiscTools -> MegaAPIException [ label = \"1\" weight = \"1\" ];\nProgressMeter -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nProgressMeter -> Transference [ label = \"2\" weight = \"2\" color = \"red\" ];\nSetMasterPasswordDialog -> MiscTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nSetMasterPasswordDialog -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nSettingsDialog -> MiscTools [ label = \"8\" weight = \"8\" color = \"red\" ];\nSettingsDialog -> MainPanel [ label = \"2\" weight = \"2\" color = \"red\" ];\nSettingsDialog -> MainPanelView [ label = \"1\" weight = \"1\" color = \"red\" ];\nSettingsDialog -> DBTools [ label = \"36\" weight = \"36\" ];\nSettingsDialog -> MegaAPI [ label = \"3\" weight = \"3\" ];\nSettingsDialog -> Get2FACode [ label = \"4\" weight = \"4\" ];\nSettingsDialog -> GetMasterPasswordDialog [ label = \"2\" weight = \"2\" ];\nSettingsDialog -> SetMasterPasswordDialog [ label = \"2\" weight = \"2\" ];\nSmartMegaProxyManager -> MainPanel [ label = \"2\" weight = \"2\" ];\nSpeedMeter -> TransferenceManager [ label = \"2\" weight = \"2\" ];\nSpeedMeter -> Transference [ label = \"5\" weight = \"5\" ];\nSqliteSingleton_LazyHolder -> SqliteSingleton [ label = \"2\" weight = \"2\" ];\nStreamChunk -> StreamChunk_ByteArrayOutInputStream [ label = \"2\" weight = \"2\" ];\nStreamChunk -> ChunkInvalidException [ label = \"1\" weight = \"1\" ];\nStreamChunkDownloader -> StreamChunkManager [ label = \"4\" weight = \"4\" ];\nStreamChunkDownloader -> SmartMegaProxyManager [ label = \"4\" weight = \"4\" color = \"red\" ];\nStreamChunkDownloader -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nStreamChunkDownloader -> StreamChunk [ label = \"2\" weight = \"2\" ];\nStreamChunkManager -> SecureMultiThreadNotifiable [ label = \"1\" weight = \"1\" ];\nStreamChunkManager -> StreamChunk [ label = \"4\" weight = \"4\" ];\nStreamChunkManager -> KissVideoStreamServer [ label = \"3\" weight = \"3\" color = \"red\" ];\nStreamerDialog -> ClipboardChangeObserver [ label = \"1\" weight = \"1\" ];\nStreamerDialog -> MiscTools [ label = \"3\" weight = \"3\" color = \"red\" ];\nStreamerDialog -> ClipboardSpy [ label = \"2\" weight = \"2\" ];\nStreamerDialog -> MainPanelView [ label = \"2\" weight = \"2\" color = \"red\" ];\nStreamerDialog -> MainPanel [ label = \"3\" weight = \"3\" color = \"red\" ];\nStreamerDialog -> CryptTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nStreamerDialog -> MegaAPI [ label = \"3\" weight = \"3\" ];\nStreamThrottlerSupervisor -> SecureMultiThreadNotifiable [ label = \"1\" weight = \"1\" ];\nStreamThrottlerSupervisor -> StreamThrottlerSupervisor_1 [ label = \"1\" weight = \"1\" ];\nThrottledInputStream -> StreamThrottlerSupervisor [ label = \"2\" weight = \"2\" ];\nThrottledOutputStream -> StreamThrottlerSupervisor [ label = \"2\" weight = \"2\" ];\nTransference -> ProgressMeter [ label = \"1\" weight = \"1\" ];\nTransference -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nTransference -> TransferenceView [ label = \"1\" weight = \"1\" ];\nTransferenceManager -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nTransferenceManager -> Transference [ label = \"39\" weight = \"39\" ];\nTransferenceManager -> MiscTools [ label = \"2\" weight = \"2\" color = \"red\" ];\nTransferenceManager -> MainPanel [ label = \"3\" weight = \"3\" color = \"red\" ];\nTransferenceManager -> BoundedExecutor [ label = \"2\" weight = \"2\" ];\nUpload -> Transference [ label = \"1\" weight = \"1\" ];\nUpload -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nUpload -> UploadView [ label = \"6\" weight = \"6\" ];\nUpload -> MiscTools [ label = \"5\" weight = \"5\" color = \"red\" ];\nUpload -> TransferenceManager [ label = \"7\" weight = \"7\" ];\nUpload -> MainPanel [ label = \"3\" weight = \"3\" color = \"red\" ];\nUpload -> ProgressMeter [ label = \"2\" weight = \"2\" ];\nUpload -> ChunkUploader [ label = \"8\" weight = \"8\" ];\nUpload -> UploadMACGenerator [ label = \"2\" weight = \"2\" ];\nUpload -> MegaAPI [ label = \"3\" weight = \"3\" color = \"red\" ];\nUpload -> DBTools [ label = \"2\" weight = \"2\" ];\nUpload -> Thumbnailer [ label = \"4\" weight = \"4\" ];\nUpload -> MegaAPIException [ label = \"3\" weight = \"3\" ];\nUpload -> ChunkInvalidException [ label = \"1\" weight = \"1\" ];\nUploadMACGenerator -> SecureSingleThreadNotifiable [ label = \"1\" weight = \"1\" ];\nUploadMACGenerator -> Upload [ label = \"3\" weight = \"3\" color = \"red\" ];\nUploadMACGenerator -> DBTools [ label = \"1\" weight = \"1\" ];\nUploadMACGenerator -> MiscTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nUploadMACGenerator -> CryptTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nUploadMACGenerator -> ChunkInvalidException [ label = \"1\" weight = \"1\" ];\nUploadManager -> TransferenceManager [ label = \"4\" weight = \"4\" ];\nUploadManager -> MiscTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nUploadManager -> Upload [ label = \"1\" weight = \"1\" ];\nUploadManager -> MainPanel [ label = \"1\" weight = \"1\" color = \"red\" ];\nUploadManager -> Transference [ label = \"3\" weight = \"3\" ];\nUploadView -> TransferenceView [ label = \"1\" weight = \"1\" ];\nUploadView -> MiscTools [ label = \"15\" weight = \"15\" color = \"red\" ];\nUploadView -> Upload [ label = \"6\" weight = \"6\" color = \"red\" ];\nWarningExitMessage -> MiscTools [ label = \"1\" weight = \"1\" color = \"red\" ];\nWarningExitMessage -> MainPanel [ label = \"3\" weight = \"3\" color = \"red\" ];\nDBTools;\nBoundedExecutor;\nStreamThrottlerSupervisor;\nGet2FACode;\nMegaAPIException;\nSetMasterPasswordDialog;\nUploadMACGenerator;\nChunkDownloaderMono;\nUploadManager;\nStreamerDialog;\nSecureMultiThreadNotifiable;\nChunkWriterManager;\nLinkGrabberDialog;\nClipboardChangeObservable;\nFileGrabberDialog;\nSecureSingleThreadNotifiable;\nMegaCrypterAPI;\nMegaDirNode;\nMegaProxyServer;\nContextMenuMouseListener__Actions;\nMegaProxyServer_Handler_1;\nSqliteSingleton_LazyHolder;\nSettingsDialog;\nProgressMeter;\nStreamChunk;\nCryptTools;\nFileMergerDialog;\nClipboardSpy;\nWarningExitMessage;\nContextMenuMouseListener;\nUploadView;\nTransference;\nTransferenceView;\nSmartMegaProxyManager;\nThrottledOutputStream;\nMegaMutableTreeNode_1;\nLabelTranslatorSingleton;\nStreamChunkDownloader;\nChunkInvalidException;\nClipboardChangeObserver;\nAboutDialog;\nContentType;\nSqliteSingleton;\nFileSplitterDialog;\nGetMasterPasswordDialog;\nThumbnailer;\nDownloadView;\nMegaMutableTreeNode;\nDownload;\nDownloadManager;\nMegaCrypterAPIException;\nSpeedMeter;\nMegaProxyServer_Handler;\nChunkDownloader;\nLabelTranslatorSingleton_LazyHolder;\nStreamChunk_ByteArrayOutInputStream;\nKissVideoStreamServer;\nUpload;\nMegaAPI;\nFolderLinkDialog;\nAPIException;\nMiscTools;\nMainPanel;\nMainPanelView;\nTransferenceManager;\nStreamChunkManager;\nThrottledInputStream;\nChunkUploader;\nStreamThrottlerSupervisor_1;\n}`;\n</script>\n<button style=\"display: block; margin: 0 auto;\"\n        onclick=\"showPopup('popup-cycleName', 'graph-container-cycleName', dot)\">\n    Show Graph Popup 1\n</button>\n\n<button style=\"display: block; margin: 0 auto;\"\n        onclick=\"createForceGraph('popup-cycleName', 'graph-container-cycleName', mb_dot)\">\n    Show 3D Graph\n</button>\n\n<div class=\"popup\" id=\"popup-cycleName\">\n    <span class=\"close-btn\" onclick=\"hidePopup()\">×</span>\n    <div id=\"graph-container-cycleName\" style=\"width: 100%; height: 100%;\"></div>\n</div>\n\n</body>\n</html>"
  },
  {
    "path": "report/src/test/resources/spriteText.html",
    "content": "<head>\n    <style> body { margin: 0; } </style>\n\n    <script src=\"https://unpkg.com/3d-force-graph\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/graphlib-dot@0.6.4/dist/graphlib-dot.min.js\"></script>\n</head>\n\n<body>\n<div id=\"3d-graph\"></div>\n\n<script type=\"module\">\n    import SpriteText from \"https://esm.sh/three-spritetext\";\n\n    const dot = `\n            digraph {\n                node [style=filled];\n                A [color=red];\n                B [color=green];\n                C [color=blue];\n                A -> B [color=purple];\n                B -> C [color=orange];\n                C -> A [color=cyan];\n            }`;\n\n    const graphlibGraph = graphlibDot.read(dot);\n\n    var nodes = [];\n    var links = [];\n\n    graphlibGraph.nodes().forEach(function(node) {\n        var nodeData = graphlibGraph.node(node);\n        nodes.push({\n            id: node,\n            color: nodeData.color || 'white',\n        });\n    });\n\n    graphlibGraph.edges().forEach(function(edge) {\n        links.push({\n            source: edge.v,\n            target: edge.w,\n            color: graphlibGraph.edge(edge).color || 'white'\n        });\n    });\n\n    const Graph = new ForceGraph3D(document.getElementById('3d-graph'))\n        .graphData({ nodes: nodes, links: links })\n        .nodeAutoColorBy('group')\n        .nodeThreeObject(node => {\n            const sprite = new SpriteText(node.id);\n            sprite.material.depthWrite = false; // make sprite background transparent\n            sprite.color = node.color;\n            sprite.textHeight = 8;\n            return sprite;\n        });\n\n    // Spread nodes a little wider\n    Graph.d3Force('charge').strength(-120);\n\n</script>\n</body>"
  },
  {
    "path": "spring-petclinic-rest-report.html",
    "content": "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n\n<head><title>Refactor First Report for spring-petclinic-rest 3.4.3 </title>\n<script async defer src=\"https://buttons.github.io/buttons.js\"></script>\n<script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js\"></script><script src=\"https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/graphlib-dot@0.6.4/dist/graphlib-dot.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/3d-force-graph\"></script>\n</head>  <body class=\"composite\">\n<div class=\"overlay\" id=\"overlay\" onclick=\"hidePopup()\"></div><script>\nfunction sugiyamaLayout(graph) {\n    var layers = [];\n    var nodeLevels = {};\n    var nodes = graph.nodes();\n    //var edges = graph.edges();\n\n    // Step 1: Assign levels to nodes\n    function assignLevels() {\n        var visited = {};\n        var stack = [];\n\n        function visit(node, level) {\n            if (visited[node]) return;\n            visited[node] = true;\n            nodeLevels[node] = level;\n            if (!layers[level]) layers[level] = [];\n            layers[level].push(node);\n            stack.push(node);\n            graph.forEachNeighbor(node, function (neighbor) {\n                visit(neighbor, level + 1);\n            });\n        }\n\n        nodes.forEach(function (node) {\n            if (!visited[node]) visit(node, 0);\n        });\n    }\n\n    // Step 2: Reduce edge crossings\n    function reduceCrossings() {\n        for (var i = 0; i < layers.length - 1; i++) {\n            var layer = layers[i];\n            var nextLayer = layers[i + 1];\n            var positions = {};\n\n            nextLayer.forEach(function (node, index) {\n                positions[node] = index;\n            });\n\n            layer.sort(function (a, b) {\n                var aPos = 0, bPos = 0;\n                graph.forEachNeighbor(a, function (neighbor) {\n                    aPos += positions[neighbor] || 0;\n                });\n                graph.forEachNeighbor(b, function (neighbor) {\n                    bPos += positions[neighbor] || 0;\n                });\n                return aPos - bPos;\n            });\n        }\n    }\n\n    // Step 3: Assign positions to nodes\n    function assignPositions() {\n        var yStep = 100;\n        var xStep = 2000;\n\n        layers.forEach(function (layer, level) {\n            var layerWidth = layer.length * xStep;\n            var offsetX = ((screen.width - 200) - layerWidth) / 2; // Centering the nodes\n\n            layer.forEach(function (node, index) {\n                graph.setNodeAttribute(node, 'x', offsetX + index * xStep);\n                graph.setNodeAttribute(node, 'y', -level * yStep);\n            });\n        });\n    }\n\n    assignLevels();\n    reduceCrossings();\n    assignPositions();\n}\n\nfunction renderGraph(dot) {\n    // Parse the DOT graph using graphlib-dot\n    const graphlibGraph = graphlibDot.read(dot);\n\n    // Convert graphlib graph to graphology graph\n    const graphologyGraph = new graphology.Graph();\n    graphlibGraph.nodes().forEach(node => {\n        const attrs = graphlibGraph.node(node);\n        graphologyGraph.addNode(node, {\n            label: node,\n            color: attrs.color,\n            // x: Math.random(),\n            // y: Math.random(),\n            size: 5,\n        });\n    });\n\n    graphlibGraph.edges().forEach(edge => {\n        const attrs = graphlibGraph.edge(edge);\n        graphologyGraph.addEdge(edge.v, edge.w, {\n            color: attrs.color,\n            size: 1,\n            type: 'arrow',\n        });\n    });\n\n    sugiyamaLayout(graphologyGraph)\n\n    return graphologyGraph;\n}\n</script><script type=\"module\">\n// SpriteText will only work as import\n        // this script block requires type=module since we are using an import\n        import SpriteText from \"https://esm.sh/three-spritetext\";\n\n        function createForceGraph(popupId, containerName, dot) {\n            // Add event listener for Escape key to close the popup\n            document.addEventListener('keydown', function (event) {\n                if (event.key === 'Escape') {\n                    hidePopup();\n                }\n            });\n\n            document.getElementById('overlay').style.display = 'block';\n            document.getElementById(popupId).style.display = 'block';\n            var container = document.getElementById(containerName);\n\n            // Parse the DOT graph using graphlib-dot\n            const graphlibGraph = graphlibDot.read(dot);\n\n            var nodes = [];\n            var links = [];\n\n            graphlibGraph.nodes().forEach(function (node) {\n                var nodeData = graphlibGraph.node(node);\n                nodes.push({\n                    id: node,\n                    color: nodeData.color || 'white',\n                });\n            });\n\n            graphlibGraph.edges().forEach(function (edge) {\n                links.push({\n                    source: edge.v,\n                    target: edge.w,\n                    color: graphlibGraph.edge(edge).color || 'white',\n                    weight: graphlibGraph.edge(edge).weight,\n                });\n            });\n\n            const gData = {\n                nodes: nodes,\n                links: links\n            };\n\n            // cross-link node objects\n            gData.links.forEach(link => {\n                const a = gData.nodes.find(node => node.id === link.source);\n                const b = gData.nodes.find(node => node.id === link.target);\n                !a.neighbors && (a.neighbors = []);\n                !b.neighbors && (b.neighbors = []);\n                a.neighbors.push(b);\n                b.neighbors.push(a);\n\n                !a.links && (a.links = []);\n                !b.links && (b.links = []);\n                a.links.push(link);\n                b.links.push(link);\n            });\n\n            const Graph = new ForceGraph3D(container)\n                .graphData(gData)\n                .nodeLabel('id')\n                .width(container.clientWidth)\n                .height(container.clientHeight);\n\n            if(gData.links.length + gData.nodes.length < 4000) {\n                console.log(gData.links.length + gData.nodes.length);\n\n\n                // use node labels instead of spheres\n                Graph.nodeThreeObject(node => {\n                    const sprite = new SpriteText(node.id);\n                    sprite.material.depthWrite = false; // make sprite background transparent\n                    sprite.color = node.color;\n                    sprite.textHeight = 4;\n                    return sprite;\n                });\n\n                // code to display weight as link text\n                // may be too much for browsers to handle\n                // Graph\n                //     .linkThreeObjectExtend(true)\n                //     .linkThreeObject(link => {\n                //         // extend link with text sprite\n                //         const sprite = new SpriteText(`${link.weight}`);\n                //         sprite.color = 'lightgrey';\n                //         sprite.textHeight = 3;\n                //         return sprite;\n                //     })\n                //     .linkPositionUpdate((sprite, {start, end}) => {\n                //         const middlePos = Object.assign(...['x', 'y', 'z'].map(c => ({\n                //             [c]: start[c] + (end[c] - start[c]) / 2 // calc middle point\n                //         })));\n                //\n                //         // Position sprite\n                //         Object.assign(sprite.position, middlePos);\n                //     });\n\n\n                // code to highlight nodes & links\n                // TODO: enable via control - see Manipulate Link Force Distance for example\n                const highlightNodes = new Set();\n                const highlightLinks = new Set();\n                let hoverNode = null;\n                Graph\n                    .nodeColor(node => highlightNodes.has(node) ? node === hoverNode ? 'rgb(255,0,0,1)' : 'rgba(255,160,0,0.8)' : 'rgba(0,255,255,0.6)')\n                    .linkWidth(link => highlightLinks.has(link) ? 4 : 1)\n                    .linkDirectionalParticles(link => highlightLinks.has(link) ? 4 : 0)\n                    .linkDirectionalParticleWidth(4)\n                    .onNodeHover(node => {\n                        // no state change\n                        if ((!node && !highlightNodes.size) || (node && hoverNode === node)) return;\n\n                        highlightNodes.clear();\n                        highlightLinks.clear();\n                        if (node) {\n                            highlightNodes.add(node);\n                            node.neighbors.forEach(neighbor => highlightNodes.add(neighbor));\n                            node.links.forEach(link => highlightLinks.add(link));\n                        }\n\n                        hoverNode = node || null;\n\n                        updateHighlight(Graph);\n                    })\n                    .onLinkHover(link => {\n                        highlightNodes.clear();\n                        highlightLinks.clear();\n\n                        if (link) {\n                            highlightLinks.add(link);\n                            highlightNodes.add(link.source);\n                            highlightNodes.add(link.target);\n                        }\n\n                        updateHighlight(Graph);\n                    });\n\n            }\n        }\n\n        // used by highlighting functionality\n        function updateHighlight(Graph) {\n            // trigger update of highlighted objects in scene\n            Graph\n                .nodeColor(Graph.nodeColor())\n                .linkWidth(Graph.linkWidth())\n                .linkDirectionalParticles(Graph.linkDirectionalParticles());\n        }\n\n        // needed to allow the button to open the graph\n        window.createForceGraph = createForceGraph;    </script><script>\n    function showPopup(popupId, containerName, dot) {\n        // Add event listener for Escape key to close the popup\n        document.addEventListener('keydown', function (event) {\n            if (event.key === 'Escape') {\n                hidePopup();\n            }\n        });                document.getElementById('overlay').style.display = 'block';\n        document.getElementById(popupId).style.display = 'block';\n\n        var graph = renderGraph(dot);\n        var container = document.getElementById(containerName);\n\n        // Render with Sigma.js\n        new Sigma(graph, container);\n    }\n\n    function hidePopup() {\n        document.getElementById('overlay').style.display = 'none';\n        var popups = document.getElementsByClassName('popup');\n        for (var i = 0; i < popups.length; i++) {\n            popups[i].style.display = 'none';\n        }\n\n        // Clear the graph containers to remove the previous graphs\n        var containers = document.querySelectorAll('[id^=\"graph-container\"]');\n        containers.forEach(function(container) {\n            while (container.firstChild) {\n                container.removeChild(container.firstChild);\n            }\n        });\n// Remove the Escape key event listener\n            document.removeEventListener('keydown', function (event) {\n                if (event.key === 'Escape') {\n                    hidePopup();\n                }\n            });    }\n</script><style>\n        /* Popup container */\n        .popup {\n            position: fixed;\n            display: none;\n            width: 95%;\n            height: 95%;\n            background-color: white;\n            border: 1px solid #ccc;\n            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n            top: 50%;\n            left: 50%;\n            transform: translate(-50%, -50%);\n            z-index: 1000;\n            padding: 20px;\n            box-sizing: border-box;\n        }\n\n        /* Popup overlay */\n        .overlay {\n            position: fixed;\n            display: none;\n            width: 100%;\n            height: 100%;\n            top: 0;\n            left: 0;\n            background: rgba(0, 0, 0, 0.5);\n            z-index: 999;\n        }\n\n        /* Close button */\n        .close-btn {\n            position: absolute;\n            top: 10px;\n            right: 10px;\n            cursor: pointer;\n        }\n    </style>    <div id=\"banner\">\n    </div>\n    <div id=\"breadcrumbs\">\n      <div class=\"xleft\">\n</div>\n      <div class=\"xright\">      </div>\n    </div>\n    <div id=\"bodyColumn\">\n      <div id=\"contentBox\">\n<section>\n<h2><a href=\"https://github.com/refactorfirst/refactorfirst\" target=\"_blank\" title=\"Learn about RefactorFirst\" aria-label=\"RefactorFirst\">RefactorFirst</a> Report for spring-petclinic-rest 3.4.3</h2>\n<a href=\"#EDGES\">Back Edges</a>\n<br/>\n<a href=\"#CBO\">Highly Coupled Classes</a>\n<br/>\n<a href=\"#CYCLES\">Class Cycles</a>\n<h1 align=\"center\">Class Map</h1><script>\nconst classGraph_dot = `strict digraph G {\nOwnerMapper -> Owner [ label = \"5\" weight = \"5\" ];\nPetMapper -> Pet [ label = \"5\" weight = \"5\" ];\nPetMapper -> PetType [ label = \"3\" weight = \"3\" ];\nPetTypeMapper -> PetType [ label = \"5\" weight = \"5\" ];\nSpecialtyMapper -> Specialty [ label = \"4\" weight = \"4\" ];\nUserMapper -> Role [ label = \"4\" weight = \"4\" ];\nUserMapper -> User [ label = \"2\" weight = \"2\" ];\nVetMapper -> Vet [ label = \"4\" weight = \"4\" ];\nVisitMapper -> Visit [ label = \"4\" weight = \"4\" ];\nNamedEntity -> BaseEntity [ label = \"1\" weight = \"1\" ];\nOwner -> Person [ label = \"1\" weight = \"1\" ];\nOwner -> Pet [ label = \"13\" weight = \"13\" color = \"red\" ];\nOwner -> NamedEntity [ label = \"1\" weight = \"1\" ];\nPerson -> BaseEntity [ label = \"1\" weight = \"1\" ];\nPet -> NamedEntity [ label = \"1\" weight = \"1\" ];\nPet -> Visit [ label = \"9\" weight = \"9\" ];\nPet -> PetType [ label = \"3\" weight = \"3\" ];\nPet -> Owner [ label = \"3\" weight = \"3\" ];\nPetType -> NamedEntity [ label = \"1\" weight = \"1\" ];\nRole -> BaseEntity [ label = \"1\" weight = \"1\" ];\nRole -> User [ label = \"3\" weight = \"3\" color = \"red\" ];\nSpecialty -> NamedEntity [ label = \"1\" weight = \"1\" ];\nUser -> Role [ label = \"6\" weight = \"6\" ];\nVet -> Person [ label = \"1\" weight = \"1\" ];\nVet -> Specialty [ label = \"8\" weight = \"8\" ];\nVisit -> BaseEntity [ label = \"1\" weight = \"1\" ];\nVisit -> Pet [ label = \"3\" weight = \"3\" color = \"red\" ];\nJdbcOwnerRepositoryImpl -> OwnerRepository [ label = \"1\" weight = \"1\" ];\nJdbcOwnerRepositoryImpl -> Owner [ label = \"13\" weight = \"13\" ];\nJdbcOwnerRepositoryImpl -> JdbcPet [ label = \"2\" weight = \"2\" ];\nJdbcOwnerRepositoryImpl -> PetType [ label = \"2\" weight = \"2\" ];\nJdbcOwnerRepositoryImpl -> Pet [ label = \"3\" weight = \"3\" ];\nJdbcOwnerRepositoryImpl -> Visit [ label = \"2\" weight = \"2\" ];\nJdbcPet -> Pet [ label = \"1\" weight = \"1\" ];\nJdbcPetRepositoryImpl -> PetRepository [ label = \"1\" weight = \"1\" ];\nJdbcPetRepositoryImpl -> EntityUtils [ label = \"1\" weight = \"1\" ];\nJdbcPetRepositoryImpl -> OwnerRepository [ label = \"3\" weight = \"3\" ];\nJdbcPetRepositoryImpl -> VisitRepository [ label = \"2\" weight = \"2\" ];\nJdbcPetRepositoryImpl -> Owner [ label = \"2\" weight = \"2\" ];\nJdbcPetRepositoryImpl -> Pet [ label = \"8\" weight = \"8\" ];\nJdbcPetRepositoryImpl -> JdbcPet [ label = \"3\" weight = \"3\" ];\nJdbcPetRepositoryImpl -> PetType [ label = \"2\" weight = \"2\" ];\nJdbcPetRepositoryImpl -> Visit [ label = \"2\" weight = \"2\" ];\nJdbcPetRowMapper -> BaseEntity [ label = \"1\" weight = \"1\" ];\nJdbcPetRowMapper -> NamedEntity [ label = \"1\" weight = \"1\" ];\nJdbcPetRowMapper -> Pet [ label = \"1\" weight = \"1\" ];\nJdbcPetRowMapper -> JdbcPet [ label = \"5\" weight = \"5\" ];\nJdbcPetTypeRepositoryImpl -> PetTypeRepository [ label = \"1\" weight = \"1\" ];\nJdbcPetTypeRepositoryImpl -> PetType [ label = \"7\" weight = \"7\" ];\nJdbcPetTypeRepositoryImpl -> Pet [ label = \"3\" weight = \"3\" ];\nJdbcPetTypeRepositoryImpl -> Visit [ label = \"3\" weight = \"3\" ];\nJdbcPetVisitExtractor -> Pet [ label = \"1\" weight = \"1\" ];\nJdbcPetVisitExtractor -> JdbcPet [ label = \"1\" weight = \"1\" ];\nJdbcPetVisitExtractor -> Visit [ label = \"1\" weight = \"1\" ];\nJdbcSpecialtyRepositoryImpl -> SpecialtyRepository [ label = \"1\" weight = \"1\" ];\nJdbcSpecialtyRepositoryImpl -> Specialty [ label = \"7\" weight = \"7\" ];\nJdbcUserRepositoryImpl -> UserRepository [ label = \"1\" weight = \"1\" ];\nJdbcUserRepositoryImpl -> User [ label = \"3\" weight = \"3\" ];\nJdbcUserRepositoryImpl -> Role [ label = \"1\" weight = \"1\" ];\nJdbcVetRepositoryImpl -> VetRepository [ label = \"1\" weight = \"1\" ];\nJdbcVetRepositoryImpl -> Vet [ label = \"9\" weight = \"9\" ];\nJdbcVetRepositoryImpl -> Specialty [ label = \"5\" weight = \"5\" ];\nJdbcVetRepositoryImpl -> EntityUtils [ label = \"2\" weight = \"2\" ];\nJdbcVisitRepositoryImpl_JdbcVisitRowMapperExt -> BaseEntity [ label = \"1\" weight = \"1\" ];\nJdbcVisitRepositoryImpl_JdbcVisitRowMapperExt -> Visit [ label = \"6\" weight = \"6\" ];\nJdbcVisitRepositoryImpl_JdbcVisitRowMapperExt -> Pet [ label = \"2\" weight = \"2\" ];\nJdbcVisitRepositoryImpl -> VisitRepository [ label = \"1\" weight = \"1\" ];\nJdbcVisitRepositoryImpl -> Visit [ label = \"9\" weight = \"9\" ];\nJdbcVisitRepositoryImpl -> JdbcPet [ label = \"1\" weight = \"1\" ];\nJdbcVisitRepositoryImpl_JdbcVisitRowMapperExt -> JdbcPet [ label = \"2\" weight = \"2\" ];\nJdbcVisitRepositoryImpl_JdbcVisitRowMapperExt -> PetType [ label = \"2\" weight = \"2\" ];\nJdbcVisitRepositoryImpl_JdbcVisitRowMapperExt -> Owner [ label = \"2\" weight = \"2\" ];\nJdbcVisitRowMapper -> BaseEntity [ label = \"1\" weight = \"1\" ];\nJdbcVisitRowMapper -> Visit [ label = \"5\" weight = \"5\" ];\nJpaOwnerRepositoryImpl -> OwnerRepository [ label = \"1\" weight = \"1\" ];\nJpaOwnerRepositoryImpl -> Owner [ label = \"5\" weight = \"5\" ];\nJpaPetRepositoryImpl -> PetRepository [ label = \"1\" weight = \"1\" ];\nJpaPetRepositoryImpl -> Pet [ label = \"4\" weight = \"4\" ];\nJpaPetRepositoryImpl -> PetType [ label = \"1\" weight = \"1\" ];\nJpaPetTypeRepositoryImpl -> PetTypeRepository [ label = \"1\" weight = \"1\" ];\nJpaPetTypeRepositoryImpl -> PetType [ label = \"5\" weight = \"5\" ];\nJpaPetTypeRepositoryImpl -> BaseEntity [ label = \"1\" weight = \"1\" ];\nJpaPetTypeRepositoryImpl -> Pet [ label = \"3\" weight = \"3\" ];\nJpaPetTypeRepositoryImpl -> Visit [ label = \"2\" weight = \"2\" ];\nJpaSpecialtyRepositoryImpl -> SpecialtyRepository [ label = \"1\" weight = \"1\" ];\nJpaSpecialtyRepositoryImpl -> Specialty [ label = \"5\" weight = \"5\" ];\nJpaSpecialtyRepositoryImpl -> BaseEntity [ label = \"1\" weight = \"1\" ];\nJpaUserRepositoryImpl -> UserRepository [ label = \"1\" weight = \"1\" ];\nJpaUserRepositoryImpl -> User [ label = \"1\" weight = \"1\" ];\nJpaVetRepositoryImpl -> VetRepository [ label = \"1\" weight = \"1\" ];\nJpaVetRepositoryImpl -> Vet [ label = \"4\" weight = \"4\" ];\nJpaVisitRepositoryImpl -> VisitRepository [ label = \"1\" weight = \"1\" ];\nJpaVisitRepositoryImpl -> Visit [ label = \"5\" weight = \"5\" ];\nOwnerRepository -> Owner [ label = \"5\" weight = \"5\" ];\nPetRepository -> Pet [ label = \"4\" weight = \"4\" ];\nPetRepository -> PetType [ label = \"1\" weight = \"1\" ];\nPetTypeRepository -> PetType [ label = \"5\" weight = \"5\" ];\nSpecialtyRepository -> Specialty [ label = \"5\" weight = \"5\" ];\nPetRepositoryOverride -> Pet [ label = \"1\" weight = \"1\" ];\nPetTypeRepositoryOverride -> PetType [ label = \"1\" weight = \"1\" ];\nSpecialtyRepositoryOverride -> Specialty [ label = \"1\" weight = \"1\" ];\nSpringDataOwnerRepository -> OwnerRepository [ label = \"1\" weight = \"1\" ];\nSpringDataOwnerRepository -> Owner [ label = \"2\" weight = \"2\" ];\nSpringDataPetRepository -> PetRepository [ label = \"1\" weight = \"1\" ];\nSpringDataPetRepository -> PetRepositoryOverride [ label = \"1\" weight = \"1\" ];\nSpringDataPetRepository -> PetType [ label = \"1\" weight = \"1\" ];\nSpringDataPetRepositoryImpl -> PetRepositoryOverride [ label = \"1\" weight = \"1\" ];\nSpringDataPetRepositoryImpl -> Pet [ label = \"1\" weight = \"1\" ];\nSpringDataPetTypeRepository -> PetTypeRepository [ label = \"1\" weight = \"1\" ];\nSpringDataPetTypeRepository -> PetTypeRepositoryOverride [ label = \"1\" weight = \"1\" ];\nSpringDataPetTypeRepositoryImpl -> PetTypeRepositoryOverride [ label = \"1\" weight = \"1\" ];\nSpringDataPetTypeRepositoryImpl -> PetType [ label = \"1\" weight = \"1\" ];\nSpringDataPetTypeRepositoryImpl -> BaseEntity [ label = \"1\" weight = \"1\" ];\nSpringDataPetTypeRepositoryImpl -> Pet [ label = \"3\" weight = \"3\" ];\nSpringDataPetTypeRepositoryImpl -> Visit [ label = \"2\" weight = \"2\" ];\nSpringDataSpecialtyRepository -> SpecialtyRepository [ label = \"1\" weight = \"1\" ];\nSpringDataSpecialtyRepository -> SpecialtyRepositoryOverride [ label = \"1\" weight = \"1\" ];\nSpringDataSpecialtyRepositoryImpl -> SpecialtyRepositoryOverride [ label = \"1\" weight = \"1\" ];\nSpringDataSpecialtyRepositoryImpl -> Specialty [ label = \"1\" weight = \"1\" ];\nSpringDataSpecialtyRepositoryImpl -> BaseEntity [ label = \"1\" weight = \"1\" ];\nSpringDataUserRepository -> UserRepository [ label = \"1\" weight = \"1\" ];\nSpringDataVetRepository -> VetRepository [ label = \"1\" weight = \"1\" ];\nSpringDataVisitRepository -> VisitRepository [ label = \"1\" weight = \"1\" ];\nSpringDataVisitRepository -> VisitRepositoryOverride [ label = \"1\" weight = \"1\" ];\nSpringDataVisitRepositoryImpl -> VisitRepositoryOverride [ label = \"1\" weight = \"1\" ];\nSpringDataVisitRepositoryImpl -> Visit [ label = \"1\" weight = \"1\" ];\nVisitRepositoryOverride -> Visit [ label = \"1\" weight = \"1\" ];\nUserRepository -> User [ label = \"1\" weight = \"1\" ];\nVetRepository -> Vet [ label = \"4\" weight = \"4\" ];\nVisitRepository -> Visit [ label = \"5\" weight = \"5\" ];\nExceptionControllerAdvice -> BindingErrorsResponse [ label = \"2\" weight = \"2\" ];\nBindingErrorsResponse -> BindingErrorsResponse_BindingError [ label = \"11\" weight = \"11\" ];\nOwnerRestController -> ClinicService [ label = \"11\" weight = \"11\" ];\nOwnerRestController -> Owner [ label = \"12\" weight = \"12\" ];\nOwnerRestController -> Person [ label = \"2\" weight = \"2\" ];\nOwnerRestController -> BaseEntity [ label = \"2\" weight = \"2\" ];\nOwnerRestController -> Pet [ label = \"5\" weight = \"5\" ];\nOwnerRestController -> NamedEntity [ label = \"1\" weight = \"1\" ];\nOwnerRestController -> Visit [ label = \"2\" weight = \"2\" ];\nOwnerRestController -> OwnerMapper [ label = \"4\" weight = \"4\" ];\nOwnerRestController -> PetMapper [ label = \"4\" weight = \"4\" ];\nOwnerRestController -> VisitMapper [ label = \"4\" weight = \"4\" ];\nPetRestController -> Pet [ label = \"5\" weight = \"5\" ];\nPetRestController -> NamedEntity [ label = \"1\" weight = \"1\" ];\nPetRestController -> ClinicService [ label = \"7\" weight = \"7\" ];\nPetRestController -> PetMapper [ label = \"4\" weight = \"4\" ];\nPetTypeRestController -> ClinicService [ label = \"8\" weight = \"8\" ];\nPetTypeRestController -> NamedEntity [ label = \"1\" weight = \"1\" ];\nPetTypeRestController -> PetTypeMapper [ label = \"3\" weight = \"3\" ];\nPetTypeRestController -> PetType [ label = \"6\" weight = \"6\" ];\nSpecialtyRestController -> ClinicService [ label = \"8\" weight = \"8\" ];\nSpecialtyRestController -> NamedEntity [ label = \"1\" weight = \"1\" ];\nSpecialtyRestController -> SpecialtyMapper [ label = \"3\" weight = \"3\" ];\nSpecialtyRestController -> Specialty [ label = \"4\" weight = \"4\" ];\nUserRestController -> UserService [ label = \"3\" weight = \"3\" ];\nUserRestController -> UserMapper [ label = \"3\" weight = \"3\" ];\nUserRestController -> User [ label = \"1\" weight = \"1\" ];\nVetRestController -> ClinicService [ label = \"10\" weight = \"10\" ];\nVetRestController -> Person [ label = \"2\" weight = \"2\" ];\nVetRestController -> Vet [ label = \"5\" weight = \"5\" ];\nVetRestController -> VetMapper [ label = \"3\" weight = \"3\" ];\nVetRestController -> SpecialtyMapper [ label = \"2\" weight = \"2\" ];\nVetRestController -> Specialty [ label = \"3\" weight = \"3\" ];\nVisitRestController -> ClinicService [ label = \"8\" weight = \"8\" ];\nVisitRestController -> Visit [ label = \"8\" weight = \"8\" ];\nVisitRestController -> VisitMapper [ label = \"3\" weight = \"3\" ];\nClinicService -> Pet [ label = \"4\" weight = \"4\" ];\nClinicService -> Visit [ label = \"5\" weight = \"5\" ];\nClinicService -> Vet [ label = \"5\" weight = \"5\" ];\nClinicService -> Owner [ label = \"5\" weight = \"5\" ];\nClinicService -> PetType [ label = \"5\" weight = \"5\" ];\nClinicService -> Specialty [ label = \"5\" weight = \"5\" ];\nClinicServiceImpl -> ClinicService [ label = \"1\" weight = \"1\" ];\nClinicServiceImpl -> PetRepository [ label = \"6\" weight = \"6\" ];\nClinicServiceImpl -> VisitRepository [ label = \"6\" weight = \"6\" ];\nClinicServiceImpl -> VetRepository [ label = \"6\" weight = \"6\" ];\nClinicServiceImpl -> OwnerRepository [ label = \"6\" weight = \"6\" ];\nClinicServiceImpl -> PetTypeRepository [ label = \"5\" weight = \"5\" ];\nClinicServiceImpl -> SpecialtyRepository [ label = \"5\" weight = \"5\" ];\nClinicServiceImpl -> Pet [ label = \"5\" weight = \"5\" ];\nClinicServiceImpl -> Visit [ label = \"5\" weight = \"5\" ];\nClinicServiceImpl -> Vet [ label = \"5\" weight = \"5\" ];\nClinicServiceImpl -> Owner [ label = \"5\" weight = \"5\" ];\nClinicServiceImpl -> PetType [ label = \"5\" weight = \"5\" ];\nClinicServiceImpl -> Specialty [ label = \"5\" weight = \"5\" ];\nUserService -> User [ label = \"1\" weight = \"1\" ];\nUserServiceImpl -> UserService [ label = \"1\" weight = \"1\" ];\nUserServiceImpl -> UserRepository [ label = \"2\" weight = \"2\" ];\nUserServiceImpl -> User [ label = \"1\" weight = \"1\" ];\nUserServiceImpl -> Role [ label = \"1\" weight = \"1\" ];\nBaseEntity -> EntityUtils [ label = \"4\" weight = \"4\" ];\nEntityUtils -> BaseEntity [ label = \"1\" weight = \"1\" color = \"red\" ];\nUserRepository;\nSpecialtyMapper;\nSpringDataPetRepositoryImpl;\nJdbcPetVisitExtractor;\nSpringDataOwnerRepository;\nJdbcPetRowMapper;\nPetTypeRepositoryOverride;\nExceptionControllerAdvice;\nUserServiceImpl;\nJdbcUserRepositoryImpl;\nVetRestController;\nVet;\nSpringDataVetRepository;\nJdbcVetRepositoryImpl;\nJpaVetRepositoryImpl;\nNamedEntity;\nJdbcOwnerRepositoryImpl;\nOwner;\nPet;\nJdbcPetTypeRepositoryImpl;\nPetRestController;\nPetRepository;\nJdbcSpecialtyRepositoryImpl;\nJpaPetRepositoryImpl;\nSpecialty;\nVisitRepository;\nJdbcVisitRepositoryImpl;\nSpecialtyRepositoryOverride;\nSpringDataPetTypeRepositoryImpl;\nJdbcPetRepositoryImpl;\nSpringDataSpecialtyRepositoryImpl;\nUser;\nClinicServiceImpl;\nVisit;\nJpaPetTypeRepositoryImpl;\nSpringDataPetRepository;\nJpaUserRepositoryImpl;\nUserRestController;\nSpecialtyRestController;\nSpringDataPetTypeRepository;\nPetTypeMapper;\nPetMapper;\nBindingErrorsResponse;\nEntityUtils;\nPetTypeRepository;\nSpringDataSpecialtyRepository;\nVisitMapper;\nRole;\nJdbcPet;\nVetRepository;\nBindingErrorsResponse_BindingError;\nPetType;\nOwnerRepository;\nJpaSpecialtyRepositoryImpl;\nOwnerRestController;\nVisitRepositoryOverride;\nClinicService;\nUserService;\nPetTypeRestController;\nSpringDataVisitRepository;\nBaseEntity;\nOwnerMapper;\nVetMapper;\nSpringDataVisitRepositoryImpl;\nJdbcVisitRepositoryImpl_JdbcVisitRowMapperExt;\nJpaVisitRepositoryImpl;\nVisitRestController;\nJdbcVisitRowMapper;\nPerson;\nSpecialtyRepository;\nJpaOwnerRepositoryImpl;\nPetRepositoryOverride;\nSpringDataUserRepository;\nUserMapper;\n}`;\n</script>\n<button style=\"display: block; margin: 0 auto;\" onclick=\"createForceGraph('popup-classGraph', 'graph-container-classGraph', classGraph_dot )\">Show classGraph 3D Popup</button>\n<button style=\"display: block; margin: 0 auto;\" onclick=\"showPopup('popup-classGraph', 'graph-container-classGraph', classGraph_dot )\">Show classGraph 2D Popup</button>\n<div class=\"popup\" id=\"popup-classGraph\">\n<span class=\"close-btn\" onclick=\"hidePopup()\">×</span>\n    <div id=\"graph-container-classGraph\" style=\"width: 100%; height: 100%;\"></div>\n</div>\n<div align=\"center\">\nRed lines represent back edges to remove.<br>\nZoom in / out with your mouse wheel and click/move to drag the image.\n</div>\n<div align=\"center\">Excludes classes that have no incoming and outgoing edges<br></div><div align=\"center\">Number of classes: 82  Number of relationships: 192<br></div><div id=\"classGraph\" style=\"width: 95%; margin: auto; border: thin solid black\"></div>\n<script type=\"module\">\nimport { Graphviz } from \"https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/index.js\";\n    if (Graphviz) {\n        const graphviz = await Graphviz.load();\n        let svg = graphviz.layout(classGraph_dot, \"svg\", \"dot\");\n        // Set desired width and height\n\n        // Modify the SVG string to include width and height attributes\n        svg = svg.replace('<svg ', `<svg width=\"screen.width\" height=\"screen.height\"`);\n\n        document.getElementById(\"classGraph\").innerHTML = svg;\n\n        // Make the SVG zoomable\n        svgPanZoom('#classGraph svg', {\n            zoomEnabled: true,\n            controlIconsEnabled: true\n        });\n    }\n</script>\n<br/>\n<div align=\"center\">\nShow RefactorFirst some &#10084;&#65039;\n<br/>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst\" data-icon=\"octicon-star\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Star refactorfirst/refactorfirst on GitHub\">Star</a>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst/fork\" data-icon=\"octicon-repo-forked\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Fork refactorfirst/refactorfirst on GitHub\">Fork</a>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst/subscription\" data-icon=\"octicon-eye\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Watch refactorfirst/refactorfirst on GitHub\">Watch</a>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst/issues\" data-icon=\"octicon-issue-opened\" data-size=\"large\" data-show-count=\"false\" aria-label=\"Issue refactorfirst/refactorfirst on GitHub\">Issue</a>\n<a class=\"github-button\" href=\"https://github.com/sponsors/jimbethancourt\" data-icon=\"octicon-heart\" data-size=\"large\" aria-label=\"Sponsor @jimbethancourt on GitHub\">Sponsor</a>\n</div><br/>\n<div style=\"text-align: center;\"><a id=\"EDGES\"><h1>Backward Edge Removal Impact</h1></a></div>\n<div style=\"text-align: center;\">\nCurrent Cycle Count: 3<br>\nCurrent Average Cycle Node Count: 2.3333333333333335<br>\nCurrent Total Back Edge Count: 4<br>\nCurrent Total Min Weight Back Edge Count: 1<br>\n</div>\n<table align=\"center\" border=\"5px\">\n<thead>\n<tr>\n<th>Edge</th>\n<th>Edge Weight</th>\n<th>In # of Cycles</th>\n<th>New Cycle Count</th>\n<th>New Avg Cycle Node Count</th>\n<th>Avg Node &Delta; &divide; Effort</th>\n</thead>\n<tbody>\n<tr>\n<td align=\"left\">Visit &#8594; Pet : 3</td>\n<td align=\"right\">3</td>\n<td align=\"right\">1</td>\n<td align=\"right\">3</td>\n<td align=\"right\">2.0</td>\n<td align=\"right\">0.11111111111111116</td>\n</tr>\n<tr>\n<td align=\"left\">Owner &#8594; Pet : 13</td>\n<td align=\"right\">13</td>\n<td align=\"right\">1</td>\n<td align=\"right\">3</td>\n<td align=\"right\">2.0</td>\n<td align=\"right\">0.025641025641025654</td>\n</tr>\n<tr>\n<td align=\"left\">Role &#8594; User : 3</td>\n<td align=\"right\">3</td>\n<td align=\"right\">1</td>\n<td align=\"right\">2</td>\n<td align=\"right\">2.5</td>\n<td align=\"right\">-0.055555555555555504</td>\n</tr>\n<tr>\n<td align=\"left\">EntityUtils &#8594; BaseEntity : 1</td>\n<td align=\"right\">1</td>\n<td align=\"right\">1</td>\n<td align=\"right\">2</td>\n<td align=\"right\">2.5</td>\n<td align=\"right\">-0.16666666666666652</td>\n</tr>\n</tbody>\n</table>\n<div align=\"center\">\nShow RefactorFirst some &#10084;&#65039;\n<br/>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst\" data-icon=\"octicon-star\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Star refactorfirst/refactorfirst on GitHub\">Star</a>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst/fork\" data-icon=\"octicon-repo-forked\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Fork refactorfirst/refactorfirst on GitHub\">Fork</a>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst/subscription\" data-icon=\"octicon-eye\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Watch refactorfirst/refactorfirst on GitHub\">Watch</a>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst/issues\" data-icon=\"octicon-issue-opened\" data-size=\"large\" data-show-count=\"false\" aria-label=\"Issue refactorfirst/refactorfirst on GitHub\">Issue</a>\n<a class=\"github-button\" href=\"https://github.com/sponsors/jimbethancourt\" data-icon=\"octicon-heart\" data-size=\"large\" aria-label=\"Sponsor @jimbethancourt on GitHub\">Sponsor</a>\n</div><br/>\n<br/>\n<br/>\n<br/>\n<hr/>\n<br/>\n<br/>\n<div style=\"text-align: center;\"><a id=\"CBO\"><h1>Highly Coupled Classes</h1></a></div><div id=\"series_chart_div_2\" align=\"center\"><script>      google.charts.load('current', {'packages':['corechart']});\n      google.charts.setOnLoadCallback(drawSeriesChart);\n\n    function drawSeriesChart() {\n\n      var data2 = google.visualization.arrayToDataTable([[ 'ID', 'Coupling Count', 'Change Proneness', 'Priority', 'Priority (Visual)'], ['ClinicServiceImpl.java',22,1,1,-1]]);\n\n      var options = {\n        title: 'Priority Ranking for Refactoring Highly Coupled Classes - ' +\n               'Start with Priority 1',\n        height: 900,         width: 1200,         explorer: {},         hAxis: {title: 'Coupling Count'},\n        vAxis: {title: 'Change Proneness'},\n        colorAxis: {colors: ['green', 'red']},\n        bubble: {textStyle: {fontSize: 11}}      };\n\n      var chart2 = new google.visualization.BubbleChart(document.getElementById('series_chart_div_2'));\n      chart2.draw(data2, options);\n    }\n</script></div>\n<div align=\"center\">\nShow RefactorFirst some &#10084;&#65039;\n<br/>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst\" data-icon=\"octicon-star\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Star refactorfirst/refactorfirst on GitHub\">Star</a>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst/fork\" data-icon=\"octicon-repo-forked\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Fork refactorfirst/refactorfirst on GitHub\">Fork</a>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst/subscription\" data-icon=\"octicon-eye\" data-size=\"large\" data-show-count=\"true\" aria-label=\"Watch refactorfirst/refactorfirst on GitHub\">Watch</a>\n<a class=\"github-button\" href=\"https://github.com/refactorfirst/refactorfirst/issues\" data-icon=\"octicon-issue-opened\" data-size=\"large\" data-show-count=\"false\" aria-label=\"Issue refactorfirst/refactorfirst on GitHub\">Issue</a>\n<a class=\"github-button\" href=\"https://github.com/sponsors/jimbethancourt\" data-icon=\"octicon-heart\" data-size=\"large\" aria-label=\"Sponsor @jimbethancourt on GitHub\">Sponsor</a>\n</div>       <h2>Coupling Between Objects Chart Legend:</h2>       <table border=\"5px\">\n          <tbody>\n            <tr><td><strong>X-Axis:</strong> Number of objects the class is coupled to</td></tr>\n            <tr><td><strong>Y-Axis:</strong> Relative churn</td></tr>\n            <tr><td><strong>Color:</strong> Priority of what to fix first</td></tr>\n            <tr><td><strong>Circle size:</strong> Priority (Visual) of what to fix first</td></tr>\n          </tbody>\n        </table>        <br/><h2 align=\"center\">Highly Coupled classes by the numbers: (Refactor starting with Priority 1)</h2><table align=\"center\" border=\"5px\"><thead><tr><th>Class</th><th>Priority</th><th>Change Proneness Rank</th><th>Coupling Count</th><th>Most Recent Commit Date</th><th>Commit Count</th></tr></thead><tbody><tr><td align=\"left\">ClinicServiceImpl.java</td>\n<td align=\"right\">1</td>\n<td align=\"right\">1</td>\n<td align=\"right\">22</td>\n<td align=\"right\">12/28/24, 11:16 AM</td>\n<td align=\"right\">23</td>\n</tr></tbody></table><br/>\n<br/>\n<br/>\n<br/>\n<hr/>\n<br/>\n<br/>\n<div style=\"text-align: center;\"><a id=\"CYCLES\"><h1>Class Cycles</h1></a></div>\n<h2 align=\"center\">Class Cycles by the numbers:</h2>\n<table align=\"center\" border=\"5px\">\n<thead>\n<tr>\n<th>Cycle Name</th>\n<th>Priority</th>\n<th>Class Count</th>\n<th>Relationship Count</th>\n</thead>\n<tbody>\n<tr>\n<td align=\"left\">Pet</td>\n<td align=\"right\">1</td>\n<td align=\"right\">3</td>\n<td align=\"right\">4</td>\n</tr>\n<tr>\n<td align=\"left\">Role</td>\n<td align=\"right\">2</td>\n<td align=\"right\">2</td>\n<td align=\"right\">2</td>\n</tr>\n<tr>\n<td align=\"left\">BaseEntity</td>\n<td align=\"right\">3</td>\n<td align=\"right\">2</td>\n<td align=\"right\">2</td>\n</tr>\n</tbody>\n</table>\n<br/>\n<br/>\n<br/>\n<hr/>\n<br/>\n<br/>\n<h2 align=\"center\">Class Cycle : Pet</h2>\n<h1 align=\"center\">Class Map</h1><script>\nconst Pet_dot = `strict digraph G {\nOwner -> Pet [ label = \"13\" weight = \"13\" color = \"red\" ];\nPet -> Visit [ label = \"9\" weight = \"9\" ];\nPet -> Owner [ label = \"3\" weight = \"3\" ];\nVisit -> Pet [ label = \"3\" weight = \"3\" color = \"red\" ];\nPet;\nOwner;\nVisit;\n}`;\n</script>\n<button style=\"display: block; margin: 0 auto;\" onclick=\"createForceGraph('popup-Pet', 'graph-container-Pet', Pet_dot )\">Show Pet 3D Popup</button>\n<button style=\"display: block; margin: 0 auto;\" onclick=\"showPopup('popup-Pet', 'graph-container-Pet', Pet_dot )\">Show Pet 2D Popup</button>\n<div class=\"popup\" id=\"popup-Pet\">\n<span class=\"close-btn\" onclick=\"hidePopup()\">×</span>\n    <div id=\"graph-container-Pet\" style=\"width: 100%; height: 100%;\"></div>\n</div>\n<div align=\"center\">\nRed lines represent back edges to remove.<br>\nZoom in / out with your mouse wheel and click/move to drag the image.\n</div>\n<div id=\"Pet\" style=\"width: 95%; margin: auto; border: thin solid black\"></div>\n<script type=\"module\">\nimport { Graphviz } from \"https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/index.js\";\n    if (Graphviz) {\n        const graphviz = await Graphviz.load();\n        let svg = graphviz.layout(Pet_dot, \"svg\", \"dot\");\n        // Set desired width and height\n\n        // Modify the SVG string to include width and height attributes\n        svg = svg.replace('<svg ', `<svg width=\"screen.width\" height=\"screen.height\"`);\n\n        document.getElementById(\"Pet\").innerHTML = svg;\n\n        // Make the SVG zoomable\n        svgPanZoom('#Pet svg', {\n            zoomEnabled: true,\n            controlIconsEnabled: true\n        });\n    }\n</script>\n<br/>\n<br/>\n<div align=\"center\"><strong>Bold text indicates back edge to remove to decompose cycle</strong><div align=\"center\">Number of classes: 3  Number of relationships: 4<br></div></div>\n<table align=\"center\" border=\"5px\">\n<thead>\n<tr>\n<th>Classes</th>\n<th>Relationships</th>\n</thead>\n<tbody>\n<tr><td align=\"left\">Pet</td>\n<td align=\"left\">Pet &#8594; Visit : 9<br/>\nPet &#8594; Owner : 3<br/>\n</td>\n</tr>\n<tr><td align=\"left\">Owner</td>\n<td align=\"left\"><strong>Owner &#8594; Pet : 13</strong><br/>\n</td>\n</tr>\n<tr><td align=\"left\">Visit</td>\n<td align=\"left\"><strong>Visit &#8594; Pet : 3</strong><br/>\n</td>\n</tr>\n</tbody>\n</table>\n<br/>\n<br/>\n<hr/>\n<br/>\n<br/>\n<h2 align=\"center\">Class Cycle : Role</h2>\n<h1 align=\"center\">Class Map</h1><script>\nconst Role_dot = `strict digraph G {\nRole -> User [ label = \"3\" weight = \"3\" color = \"red\" ];\nUser -> Role [ label = \"6\" weight = \"6\" ];\nRole;\nUser;\n}`;\n</script>\n<button style=\"display: block; margin: 0 auto;\" onclick=\"createForceGraph('popup-Role', 'graph-container-Role', Role_dot )\">Show Role 3D Popup</button>\n<button style=\"display: block; margin: 0 auto;\" onclick=\"showPopup('popup-Role', 'graph-container-Role', Role_dot )\">Show Role 2D Popup</button>\n<div class=\"popup\" id=\"popup-Role\">\n<span class=\"close-btn\" onclick=\"hidePopup()\">×</span>\n    <div id=\"graph-container-Role\" style=\"width: 100%; height: 100%;\"></div>\n</div>\n<div align=\"center\">\nRed lines represent back edges to remove.<br>\nZoom in / out with your mouse wheel and click/move to drag the image.\n</div>\n<div id=\"Role\" style=\"width: 95%; margin: auto; border: thin solid black\"></div>\n<script type=\"module\">\nimport { Graphviz } from \"https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/index.js\";\n    if (Graphviz) {\n        const graphviz = await Graphviz.load();\n        let svg = graphviz.layout(Role_dot, \"svg\", \"dot\");\n        // Set desired width and height\n\n        // Modify the SVG string to include width and height attributes\n        svg = svg.replace('<svg ', `<svg width=\"screen.width\" height=\"screen.height\"`);\n\n        document.getElementById(\"Role\").innerHTML = svg;\n\n        // Make the SVG zoomable\n        svgPanZoom('#Role svg', {\n            zoomEnabled: true,\n            controlIconsEnabled: true\n        });\n    }\n</script>\n<br/>\n<br/>\n<div align=\"center\"><strong>Bold text indicates back edge to remove to decompose cycle</strong><div align=\"center\">Number of classes: 2  Number of relationships: 2<br></div></div>\n<table align=\"center\" border=\"5px\">\n<thead>\n<tr>\n<th>Classes</th>\n<th>Relationships</th>\n</thead>\n<tbody>\n<tr><td align=\"left\">Role</td>\n<td align=\"left\"><strong>Role &#8594; User : 3</strong><br/>\n</td>\n</tr>\n<tr><td align=\"left\">User</td>\n<td align=\"left\">User &#8594; Role : 6<br/>\n</td>\n</tr>\n</tbody>\n</table>\n<br/>\n<br/>\n<hr/>\n<br/>\n<br/>\n<h2 align=\"center\">Class Cycle : BaseEntity</h2>\n<h1 align=\"center\">Class Map</h1><script>\nconst BaseEntity_dot = `strict digraph G {\nBaseEntity -> EntityUtils [ label = \"4\" weight = \"4\" ];\nEntityUtils -> BaseEntity [ label = \"1\" weight = \"1\" color = \"red\" ];\nBaseEntity;\nEntityUtils;\n}`;\n</script>\n<button style=\"display: block; margin: 0 auto;\" onclick=\"createForceGraph('popup-BaseEntity', 'graph-container-BaseEntity', BaseEntity_dot )\">Show BaseEntity 3D Popup</button>\n<button style=\"display: block; margin: 0 auto;\" onclick=\"showPopup('popup-BaseEntity', 'graph-container-BaseEntity', BaseEntity_dot )\">Show BaseEntity 2D Popup</button>\n<div class=\"popup\" id=\"popup-BaseEntity\">\n<span class=\"close-btn\" onclick=\"hidePopup()\">×</span>\n    <div id=\"graph-container-BaseEntity\" style=\"width: 100%; height: 100%;\"></div>\n</div>\n<div align=\"center\">\nRed lines represent back edges to remove.<br>\nZoom in / out with your mouse wheel and click/move to drag the image.\n</div>\n<div id=\"BaseEntity\" style=\"width: 95%; margin: auto; border: thin solid black\"></div>\n<script type=\"module\">\nimport { Graphviz } from \"https://cdn.jsdelivr.net/npm/@hpcc-js/wasm/dist/index.js\";\n    if (Graphviz) {\n        const graphviz = await Graphviz.load();\n        let svg = graphviz.layout(BaseEntity_dot, \"svg\", \"dot\");\n        // Set desired width and height\n\n        // Modify the SVG string to include width and height attributes\n        svg = svg.replace('<svg ', `<svg width=\"screen.width\" height=\"screen.height\"`);\n\n        document.getElementById(\"BaseEntity\").innerHTML = svg;\n\n        // Make the SVG zoomable\n        svgPanZoom('#BaseEntity svg', {\n            zoomEnabled: true,\n            controlIconsEnabled: true\n        });\n    }\n</script>\n<br/>\n<br/>\n<div align=\"center\"><strong>Bold text indicates back edge to remove to decompose cycle</strong><div align=\"center\">Number of classes: 2  Number of relationships: 2<br></div></div>\n<table align=\"center\" border=\"5px\">\n<thead>\n<tr>\n<th>Classes</th>\n<th>Relationships</th>\n</thead>\n<tbody>\n<tr><td align=\"left\">BaseEntity</td>\n<td align=\"left\">BaseEntity &#8594; EntityUtils : 4<br/>\n</td>\n</tr>\n<tr><td align=\"left\">EntityUtils</td>\n<td align=\"left\"><strong>EntityUtils &#8594; BaseEntity : 1</strong><br/>\n</td>\n</tr>\n</tbody>\n</table>\n</section>\n      <div class=\"clear\">\n        <hr/>\n      </div>\n<span id=\"publishDate\">Last Published: 4/2/25, 7:40 PM      <div class=\"clear\">\n        <hr/>\n      </div>\n</span></div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "test-resources/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.hjug.refactorfirst</groupId>\n        <artifactId>refactor-first</artifactId>\n        <version>0.8.1-SNAPSHOT</version>\n    </parent>\n\n    <groupId>org.hjug.refactorfirst.testresources</groupId>\n    <artifactId>test-resources</artifactId>\n\n    <name>RefactorFirst Test Resources</name>\n\n</project>"
  },
  {
    "path": "test-resources/src/main/resources/AttributeHandler.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.myfaces.tobago.facelets;\n\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.apache.myfaces.tobago.component.Attributes;\nimport org.apache.myfaces.tobago.component.SupportsMarkup;\nimport org.apache.myfaces.tobago.component.SupportsRenderedPartially;\nimport org.apache.myfaces.tobago.context.Markup;\nimport org.apache.myfaces.tobago.el.ConstantMethodBinding;\nimport org.apache.myfaces.tobago.internal.util.StringUtils;\nimport org.apache.myfaces.tobago.util.ComponentUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ELException;\nimport javax.el.ExpressionFactory;\nimport javax.el.MethodExpression;\nimport javax.el.ValueExpression;\nimport javax.faces.FacesException;\nimport javax.faces.component.ActionSource;\nimport javax.faces.component.ActionSource2;\nimport javax.faces.component.EditableValueHolder;\nimport javax.faces.component.UIComponent;\nimport javax.faces.component.ValueHolder;\nimport javax.faces.convert.Converter;\nimport javax.faces.event.MethodExpressionActionListener;\nimport javax.faces.event.MethodExpressionValueChangeListener;\nimport javax.faces.validator.MethodExpressionValidator;\nimport javax.faces.view.facelets.ComponentHandler;\nimport javax.faces.view.facelets.FaceletContext;\nimport javax.faces.view.facelets.TagAttribute;\nimport javax.faces.view.facelets.TagConfig;\nimport javax.faces.view.facelets.TagException;\nimport javax.faces.view.facelets.TagHandler;\nimport java.beans.IntrospectionException;\nimport java.beans.PropertyDescriptor;\n\n//from Apache MyFaces 2.0.8\n//Retrieved from http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/facelets/AttributeHandler.java/?v=source\npublic final class AttributeHandler extends TagHandler {\n\n    private static final Logger LOG = LoggerFactory.getLogger(AttributeHandler.class);\n\n    private final TagAttribute name;\n\n    private final TagAttribute value;\n\n    private final TagAttribute mode;\n\n    public AttributeHandler(final TagConfig config) {\n        super(config);\n        this.name = getRequiredAttribute(Attributes.NAME);\n        this.value = getRequiredAttribute(Attributes.VALUE);\n        this.mode = getAttribute(Attributes.MODE);\n    }\n\n    public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {\n        if (parent == null) {\n            throw new TagException(tag, \"Parent UIComponent was null\");\n        }\n\n        if (ComponentHandler.isNew(parent)) {\n\n            if (mode != null) {\n                if (\"isNotSet\".equals(mode.getValue())) {\n                    boolean result = false;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = true;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = false;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"isSet\".equals(mode.getValue())) {\n                    boolean result = true;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = false;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = true;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isNotEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"action\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                // when the action hasn't been set while using a composition.\n                                if (LOG.isDebugEnabled()) {\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (\"actionListener\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                if (LOG.isDebugEnabled()) {\n                                    // when the action hasn't been set while using a composition.\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            LOG.warn(\"Only expressions are supported mode=actionListener value='\" + expressionString + \"'\");\n                            expressionString = null;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression actionListener\n                                = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));\n                    }\n                } else if (\"actionFromValue\".equals(mode.getValue())) {\n                    if (!value.isLiteral()) {\n                        final String result = value.getValue(faceletContext);\n                        parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));\n                    }\n                } else if (\"valueIfSet\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    String lastExpressionString = null;\n                    while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {\n                        final ValueExpression expression\n                                = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                        if (expression != null) {\n                            lastExpressionString = expressionString;\n                            expressionString = expression.getExpressionString();\n                        } else {\n                            // restore last value\n                            expressionString = lastExpressionString;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final String attributeName = name.getValue(faceletContext);\n                        if (containsMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(attributeName, expression);\n                        } else {\n                            final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);\n                            parent.getAttributes().put(attributeName, literalValue);\n                        }\n                    }\n                } else {\n                    throw new FacesException(\"Type \" + mode + \" not supported\");\n                }\n            } else {\n\n                final String nameValue = name.getValue(faceletContext);\n                if (Attributes.RENDERED.equals(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.setRendered(value.getBoolean(faceletContext));\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));\n                    }\n                } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)\n                        && parent instanceof SupportsRenderedPartially) {\n\n                    if (value.isLiteral()) {\n                        final String[] components = ComponentUtils.splitList(value.getValue());\n                        ((SupportsRenderedPartially) parent).setRenderedPartially(components);\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                } else if (Attributes.STYLE_CLASS.equals(nameValue)) {\n                    // TODO expression\n                    ComponentUtils.setStyleClasses(parent, value.getValue());\n                } else if (Attributes.MARKUP.equals(nameValue)) {\n                    if (parent instanceof SupportsMarkup) {\n                        if (value.isLiteral()) {\n                            ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));\n                        } else {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(nameValue, expression);\n                        }\n                    } else {\n                        LOG.error(\"Component is not instanceof SupportsMarkup. Instance is: \" + parent.getClass().getName());\n                    }\n                } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {\n                    final MethodExpression methodExpression\n                            = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));\n                    }\n                } else if (parent instanceof EditableValueHolder\n                        && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {\n                    final MethodExpression methodExpression =\n                            getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValueChangeListener(\n                                new MethodExpressionValueChangeListener(methodExpression));\n                    }\n                } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {\n                    setConverter(faceletContext, parent, nameValue);\n                } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {\n                    final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);\n                    if (action != null) {\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {\n                    final MethodExpression action\n                            = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);\n                    if (action != null) {\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));\n                    }\n                } else if (!parent.getAttributes().containsKey(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.getAttributes().put(nameValue, value.getValue());\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                }\n            }\n        }\n    }\n\n    private boolean isMethodOrValueExpression(final String string) {\n        return (string.startsWith(\"${\") || string.startsWith(\"#{\")) && string.endsWith(\"}\");\n    }\n\n    private boolean containsMethodOrValueExpression(final String string) {\n        return (string.contains(\"${\") || string.contains(\"#{\")) && string.contains(\"}\");\n    }\n\n    private boolean isSimpleExpression(final String string) {\n        return string.indexOf('.') < 0 && string.indexOf('[') < 0;\n    }\n\n    private String removeElParenthesis(final String string) {\n        return string.substring(2, string.length() - 1);\n    }\n\n    private ValueExpression getExpression(final FaceletContext faceletContext) {\n        final String myValue = removeElParenthesis(value.getValue());\n        return faceletContext.getVariableMapper().resolveVariable(myValue);\n    }\n\n    private MethodExpression getMethodExpression(\n            final FaceletContext faceletContext, final Class returnType, final Class[] args) {\n        // in a composition may be we get the method expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,\n                        expression.getExpressionString(), returnType, args));\n            } else {\n                return null;\n            }\n        } else {\n            return value.getMethodExpression(faceletContext, returnType, args);\n        }\n    }\n\n    private Object getValue(\n            final FaceletContext faceletContext, final UIComponent parent, final String expressionString,\n            final String attributeName) {\n        Class type = Object.class;\n        try {\n            type = PropertyUtils.getReadMethod(\n                    new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();\n        } catch (final IntrospectionException e) {\n            LOG.warn(\"Can't determine expected type\", e);\n        }\n        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n        final ValueExpression valueExpression = expressionFactory\n                .createValueExpression(faceletContext, expressionString, type);\n        return valueExpression.getValue(faceletContext);\n    }\n\n    private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {\n        // in a composition may be we get the converter expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                setConverter(faceletContext, parent, nameValue, expression);\n            }\n        } else {\n            setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));\n        }\n    }\n\n    private void setConverter(\n            final FaceletContext faceletContext, final UIComponent parent, final String nameValue,\n            final ValueExpression expression) {\n        if (expression.isLiteralText()) {\n            final Converter converter =\n                    faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());\n            ((ValueHolder) parent).setConverter(converter);\n        } else {\n            parent.setValueExpression(nameValue, expression);\n        }\n    }\n}"
  },
  {
    "path": "test-resources/src/main/resources/AttributeHandler2.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.myfaces.tobago.facelets;\n\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.apache.myfaces.tobago.component.Attributes;\nimport org.apache.myfaces.tobago.component.SupportsMarkup;\nimport org.apache.myfaces.tobago.component.SupportsRenderedPartially;\nimport org.apache.myfaces.tobago.context.Markup;\nimport org.apache.myfaces.tobago.el.ConstantMethodBinding;\nimport org.apache.myfaces.tobago.internal.util.StringUtils;\nimport org.apache.myfaces.tobago.util.ComponentUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ELException;\nimport javax.el.ExpressionFactory;\nimport javax.el.MethodExpression;\nimport javax.el.ValueExpression;\nimport javax.faces.FacesException;\nimport javax.faces.component.ActionSource;\nimport javax.faces.component.ActionSource2;\nimport javax.faces.component.EditableValueHolder;\nimport javax.faces.component.UIComponent;\nimport javax.faces.component.ValueHolder;\nimport javax.faces.convert.Converter;\nimport javax.faces.event.MethodExpressionActionListener;\nimport javax.faces.event.MethodExpressionValueChangeListener;\nimport javax.faces.validator.MethodExpressionValidator;\nimport javax.faces.view.facelets.ComponentHandler;\nimport javax.faces.view.facelets.FaceletContext;\nimport javax.faces.view.facelets.TagAttribute;\nimport javax.faces.view.facelets.TagConfig;\nimport javax.faces.view.facelets.TagException;\nimport javax.faces.view.facelets.TagHandler;\nimport java.beans.IntrospectionException;\nimport java.beans.PropertyDescriptor;\n\n//from Apache MyFaces 2.0.8\n//Retrieved from http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/facelets/AttributeHandler.java/?v=source\npublic final class AttributeHandler extends TagHandler {\n\n    private static final Logger LOG = LoggerFactory.getLogger(AttributeHandler.class);\n\n    private final TagAttribute name;\n\n    private final TagAttribute value;\n\n    private final TagAttribute mode;\n\n    public AttributeHandler(final TagConfig config) {\n        super(config);\n        this.name = getRequiredAttribute(Attributes.NAME);\n        this.value = getRequiredAttribute(Attributes.VALUE);\n        this.mode = getAttribute(Attributes.MODE);\n    }\n\n    public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {\n        if (parent == null) {\n            throw new TagException(tag, \"Parent UIComponent was null\");\n        }\n\n        if (ComponentHandler.isNew(parent)) {\n\n            if (mode != null) {\n                if (\"isNotSet\".equals(mode.getValue())) {\n                    boolean result = false;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = true;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = false;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"isSet\".equals(mode.getValue())) {\n                    boolean result = true;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = false;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = true;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isNotEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"action\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                // when the action hasn't been set while using a composition.\n                                if (LOG.isDebugEnabled()) {\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (\"actionListener\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                if (LOG.isDebugEnabled()) {\n                                    // when the action hasn't been set while using a composition.\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            LOG.warn(\"Only expressions are supported mode=actionListener value='\" + expressionString + \"'\");\n                            expressionString = null;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression actionListener\n                                = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));\n                    }\n                } else if (\"actionFromValue\".equals(mode.getValue())) {\n                    if (!value.isLiteral()) {\n                        final String result = value.getValue(faceletContext);\n                        parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));\n                    }\n                } else if (\"valueIfSet\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    String lastExpressionString = null;\n                    while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {\n                        final ValueExpression expression\n                                = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                        if (expression != null) {\n                            lastExpressionString = expressionString;\n                            expressionString = expression.getExpressionString();\n                        } else {\n                            // restore last value\n                            expressionString = lastExpressionString;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final String attributeName = name.getValue(faceletContext);\n                        if (containsMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(attributeName, expression);\n                        } else {\n                            final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);\n                            parent.getAttributes().put(attributeName, literalValue);\n                        }\n                    }\n                } else {\n                    throw new FacesException(\"Type \" + mode + \" not supported\");\n                }\n            } else {\n\n                final String nameValue = name.getValue(faceletContext);\n                if (Attributes.RENDERED.equals(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.setRendered(value.getBoolean(faceletContext));\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));\n                    }\n                } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)\n                        && parent instanceof SupportsRenderedPartially) {\n\n                    if (value.isLiteral()) {\n                        final String[] components = ComponentUtils.splitList(value.getValue());\n                        ((SupportsRenderedPartially) parent).setRenderedPartially(components);\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                } else if (Attributes.STYLE_CLASS.equals(nameValue)) {\n                    // TODO expression\n                    ComponentUtils.setStyleClasses(parent, value.getValue());\n                } else if (Attributes.MARKUP.equals(nameValue)) {\n                    if (parent instanceof SupportsMarkup) {\n                        if (value.isLiteral()) {\n                            ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));\n                        } else {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(nameValue, expression);\n                        }\n                    } else {\n                        LOG.error(\"Component is not instanceof SupportsMarkup. Instance is: \" + parent.getClass().getName());\n                    }\n                } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {\n                    final MethodExpression methodExpression\n                            = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));\n                    }\n                } else if (parent instanceof EditableValueHolder\n                        && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {\n                    final MethodExpression methodExpression =\n                            getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValueChangeListener(\n                                new MethodExpressionValueChangeListener(methodExpression));\n                    }\n                } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {\n                    setConverter(faceletContext, parent, nameValue);\n                } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {\n                    final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);\n                    if (action != null) {\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {\n                    final MethodExpression action\n                            = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);\n                    if (action != null) {\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));\n                    }\n                } else if (!parent.getAttributes().containsKey(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.getAttributes().put(nameValue, value.getValue());\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                }\n            }\n        }\n    }\n\n    private boolean isMethodOrValueExpression(final String string) {\n        return (string.startsWith(\"${\") || string.startsWith(\"#{\")) && string.endsWith(\"}\");\n    }\n\n    private boolean containsMethodOrValueExpression(final String string) {\n        return (string.contains(\"${\") || string.contains(\"#{\")) && string.contains(\"}\");\n    }\n\n    private boolean isSimpleExpression(final String string) {\n        return string.indexOf('.') < 0 && string.indexOf('[') < 0;\n    }\n\n    private String removeElParenthesis(final String string) {\n        return string.substring(2, string.length() - 1);\n    }\n\n    private ValueExpression getExpression(final FaceletContext faceletContext) {\n        final String myValue = removeElParenthesis(value.getValue());\n        return faceletContext.getVariableMapper().resolveVariable(myValue);\n    }\n\n    private MethodExpression getMethodExpression(\n            final FaceletContext faceletContext, final Class returnType, final Class[] args) {\n        // in a composition may be we get the method expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,\n                        expression.getExpressionString(), returnType, args));\n            } else {\n                return null;\n            }\n        } else {\n            return value.getMethodExpression(faceletContext, returnType, args);\n        }\n    }\n\n    private Object getValue(\n            final FaceletContext faceletContext, final UIComponent parent, final String expressionString,\n            final String attributeName) {\n        Class type = Object.class;\n        try {\n            type = PropertyUtils.getReadMethod(\n                    new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();\n        } catch (final IntrospectionException e) {\n            LOG.warn(\"Can't determine expected type\", e);\n        }\n        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n        final ValueExpression valueExpression = expressionFactory\n                .createValueExpression(faceletContext, expressionString, type);\n        return valueExpression.getValue(faceletContext);\n    }\n\n    private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {\n        // in a composition may be we get the converter expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                setConverter(faceletContext, parent, nameValue, expression);\n            }\n        } else {\n            setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));\n        }\n    }\n\n    private void setConverter(\n            final FaceletContext faceletContext, final UIComponent parent, final String nameValue,\n            final ValueExpression expression) {\n        if (expression.isLiteralText()) {\n            final Converter converter =\n                    faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());\n            ((ValueHolder) parent).setConverter(converter);\n        } else {\n            parent.setValueExpression(nameValue, expression);\n        }\n    }\n\n    public static void letsAddASimpleMethod() {\n        System.out.println(\"Howdy!\");\n    }\n}"
  },
  {
    "path": "test-resources/src/main/resources/AttributeHandlerAndSorter.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.myfaces.tobago.component;\n\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.apache.myfaces.tobago.component.Attributes;\nimport org.apache.myfaces.tobago.component.SupportsMarkup;\nimport org.apache.myfaces.tobago.component.SupportsRenderedPartially;\nimport org.apache.myfaces.tobago.context.Markup;\nimport org.apache.myfaces.tobago.el.ConstantMethodBinding;\nimport org.apache.myfaces.tobago.internal.util.StringUtils;\nimport org.apache.myfaces.tobago.util.ComponentUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ELException;\nimport javax.el.ExpressionFactory;\nimport javax.el.MethodExpression;\nimport javax.el.ValueExpression;\nimport javax.faces.FacesException;\nimport javax.faces.component.ActionSource;\nimport javax.faces.component.ActionSource2;\nimport javax.faces.component.EditableValueHolder;\nimport javax.faces.component.UIComponent;\nimport javax.faces.component.ValueHolder;\nimport javax.faces.convert.Converter;\nimport javax.faces.event.MethodExpressionActionListener;\nimport javax.faces.event.MethodExpressionValueChangeListener;\nimport javax.faces.validator.MethodExpressionValidator;\nimport javax.faces.view.facelets.ComponentHandler;\nimport javax.faces.view.facelets.FaceletContext;\nimport javax.faces.view.facelets.TagAttribute;\nimport javax.faces.view.facelets.TagConfig;\nimport javax.faces.view.facelets.TagException;\nimport javax.faces.view.facelets.TagHandler;\nimport java.beans.IntrospectionException;\nimport java.beans.PropertyDescriptor;\n\nimport org.apache.myfaces.tobago.event.SortActionEvent;\nimport org.apache.myfaces.tobago.internal.component.AbstractUICommand;\nimport org.apache.myfaces.tobago.internal.component.AbstractUISheet;\nimport org.apache.myfaces.tobago.internal.util.StringUtils;\nimport org.apache.myfaces.tobago.model.SheetState;\nimport org.apache.myfaces.tobago.util.BeanComparator;\nimport org.apache.myfaces.tobago.util.ValueExpressionComparator;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ValueExpression;\nimport javax.faces.component.UIColumn;\nimport javax.faces.component.UICommand;\nimport javax.faces.component.UIComponent;\nimport javax.faces.component.UIInput;\nimport javax.faces.component.UIOutput;\nimport javax.faces.component.UISelectBoolean;\nimport javax.faces.component.UISelectMany;\nimport javax.faces.component.UISelectOne;\nimport javax.faces.context.FacesContext;\nimport javax.faces.model.DataModel;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\n\n//From http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/facelets/AttributeHandler.java/?v=source\npublic final class AttributeHandler extends TagHandler {\n\n    private static final Logger LOG = LoggerFactory.getLogger(org.apache.myfaces.tobago.facelets.AttributeHandler.class);\n\n    private final TagAttribute name;\n\n    private final TagAttribute value;\n\n    private final TagAttribute mode;\n\n    public AttributeHandler(final TagConfig config) {\n        super(config);\n        this.name = getRequiredAttribute(Attributes.NAME);\n        this.value = getRequiredAttribute(Attributes.VALUE);\n        this.mode = getAttribute(Attributes.MODE);\n    }\n\n    public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {\n        if (parent == null) {\n            throw new TagException(tag, \"Parent UIComponent was null\");\n        }\n\n        if (ComponentHandler.isNew(parent)) {\n\n            if (mode != null) {\n                if (\"isNotSet\".equals(mode.getValue())) {\n                    boolean result = false;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = true;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = false;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"isSet\".equals(mode.getValue())) {\n                    boolean result = true;\n                    String expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = false;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = true;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isNotEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"action\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                // when the action hasn't been set while using a composition.\n                                if (LOG.isDebugEnabled()) {\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (\"actionListener\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                if (LOG.isDebugEnabled()) {\n                                    // when the action hasn't been set while using a composition.\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            LOG.warn(\"Only expressions are supported mode=actionListener value='\" + expressionString + \"'\");\n                            expressionString = null;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression actionListener\n                                = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));\n                    }\n                } else if (\"actionFromValue\".equals(mode.getValue())) {\n                    if (!value.isLiteral()) {\n                        final String result = value.getValue(faceletContext);\n                        parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));\n                    }\n                } else if (\"valueIfSet\".equals(mode.getValue())) {\n                    String expressionString = value.getValue();\n                    String lastExpressionString = null;\n                    while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {\n                        final ValueExpression expression\n                                = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                        if (expression != null) {\n                            lastExpressionString = expressionString;\n                            expressionString = expression.getExpressionString();\n                        } else {\n                            // restore last value\n                            expressionString = lastExpressionString;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final String attributeName = name.getValue(faceletContext);\n                        if (containsMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(attributeName, expression);\n                        } else {\n                            final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);\n                            parent.getAttributes().put(attributeName, literalValue);\n                        }\n                    }\n                } else {\n                    throw new FacesException(\"Type \" + mode + \" not supported\");\n                }\n            } else {\n\n                final String nameValue = name.getValue(faceletContext);\n                if (Attributes.RENDERED.equals(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.setRendered(value.getBoolean(faceletContext));\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));\n                    }\n                } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)\n                        && parent instanceof SupportsRenderedPartially) {\n\n                    if (value.isLiteral()) {\n                        final String[] components = ComponentUtils.splitList(value.getValue());\n                        ((SupportsRenderedPartially) parent).setRenderedPartially(components);\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                } else if (Attributes.STYLE_CLASS.equals(nameValue)) {\n                    // TODO expression\n                    ComponentUtils.setStyleClasses(parent, value.getValue());\n                } else if (Attributes.MARKUP.equals(nameValue)) {\n                    if (parent instanceof SupportsMarkup) {\n                        if (value.isLiteral()) {\n                            ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));\n                        } else {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(nameValue, expression);\n                        }\n                    } else {\n                        LOG.error(\"Component is not instanceof SupportsMarkup. Instance is: \" + parent.getClass().getName());\n                    }\n                } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {\n                    final MethodExpression methodExpression\n                            = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));\n                    }\n                } else if (parent instanceof EditableValueHolder\n                        && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {\n                    final MethodExpression methodExpression =\n                            getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValueChangeListener(\n                                new MethodExpressionValueChangeListener(methodExpression));\n                    }\n                } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {\n                    setConverter(faceletContext, parent, nameValue);\n                } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {\n                    final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);\n                    if (action != null) {\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {\n                    final MethodExpression action\n                            = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);\n                    if (action != null) {\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));\n                    }\n                } else if (!parent.getAttributes().containsKey(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.getAttributes().put(nameValue, value.getValue());\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                }\n            }\n        }\n    }\n\n    private boolean isMethodOrValueExpression(final String string) {\n        return (string.startsWith(\"${\") || string.startsWith(\"#{\")) && string.endsWith(\"}\");\n    }\n\n    private boolean containsMethodOrValueExpression(final String string) {\n        return (string.contains(\"${\") || string.contains(\"#{\")) && string.contains(\"}\");\n    }\n\n    private boolean isSimpleExpression(final String string) {\n        return string.indexOf('.') < 0 && string.indexOf('[') < 0;\n    }\n\n    private String removeElParenthesis(final String string) {\n        return string.substring(2, string.length() - 1);\n    }\n\n    private ValueExpression getExpression(final FaceletContext faceletContext) {\n        final String myValue = removeElParenthesis(value.getValue());\n        return faceletContext.getVariableMapper().resolveVariable(myValue);\n    }\n\n    private MethodExpression getMethodExpression(\n            final FaceletContext faceletContext, final Class returnType, final Class[] args) {\n        // in a composition may be we get the method expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,\n                        expression.getExpressionString(), returnType, args));\n            } else {\n                return null;\n            }\n        } else {\n            return value.getMethodExpression(faceletContext, returnType, args);\n        }\n    }\n\n    private Object getValue(\n            final FaceletContext faceletContext, final UIComponent parent, final String expressionString,\n            final String attributeName) {\n        Class type = Object.class;\n        try {\n            type = PropertyUtils.getReadMethod(\n                    new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();\n        } catch (final IntrospectionException e) {\n            LOG.warn(\"Can't determine expected type\", e);\n        }\n        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n        final ValueExpression valueExpression = expressionFactory\n                .createValueExpression(faceletContext, expressionString, type);\n        return valueExpression.getValue(faceletContext);\n    }\n\n    private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {\n        // in a composition may be we get the converter expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                setConverter(faceletContext, parent, nameValue, expression);\n            }\n        } else {\n            setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));\n        }\n    }\n\n    private void setConverter(\n            final FaceletContext faceletContext, final UIComponent parent, final String nameValue,\n            final ValueExpression expression) {\n        if (expression.isLiteralText()) {\n            final Converter converter =\n                    faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());\n            ((ValueHolder) parent).setConverter(converter);\n        } else {\n            parent.setValueExpression(nameValue, expression);\n        }\n    }\n}\n\n//http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/component/Sorter.java/?v=source\nclass Sorter {\n\n    private static final Logger LOG = LoggerFactory.getLogger(Sorter.class);\n\n    private Comparator comparator;\n\n    /**\n     * @deprecated Please use {@link #perform(org.apache.myfaces.tobago.internal.component.AbstractUISheet)}\n     */\n    @Deprecated\n    public void perform(final SortActionEvent sortEvent) {\n        final AbstractUISheet data = (AbstractUISheet) sortEvent.getComponent();\n        perform(data);\n    }\n\n    public void perform(final AbstractUISheet data) {\n\n        Object value = data.getValue();\n        if (value instanceof DataModel) {\n            value = ((DataModel) value).getWrappedData();\n        }\n        final FacesContext facesContext = FacesContext.getCurrentInstance();\n        final SheetState sheetState = data.getSheetState(facesContext);\n\n        final String sortedColumnId = sheetState.getSortedColumnId();\n        if (LOG.isDebugEnabled()) {\n            LOG.debug(\"sorterId = '{}'\", sortedColumnId);\n        }\n\n        if (sortedColumnId == null) {\n            // not to be sorted\n            return;\n        }\n\n        final UIColumn column = (UIColumn) data.findComponent(sortedColumnId);\n        if (column == null) {\n            LOG.warn(\"No column to sort found, sorterId = '{}'\", sortedColumnId);\n            return;\n        }\n\n        final Comparator actualComparator;\n\n        if (value instanceof List || value instanceof Object[]) {\n            final String sortProperty;\n\n            try {\n                final UIComponent child = getFirstSortableChild(column.getChildren());\n                if (child != null) {\n\n                    final String attributeName = child instanceof AbstractUICommand ? Attributes.LABEL : Attributes.VALUE;\n                    if (child.getValueExpression(attributeName) != null) {\n                        final String var = data.getVar();\n                        if (var == null) {\n                            LOG.error(\"No sorting performed. Property var of sheet is not set!\");\n                            unsetSortableAttribute(column);\n                            return;\n                        }\n                        String expressionString = child.getValueExpression(attributeName).getExpressionString();\n                        if (isSimpleProperty(expressionString)) {\n                            if (expressionString.startsWith(\"#{\")\n                                    && expressionString.endsWith(\"}\")) {\n                                expressionString =\n                                        expressionString.substring(2,\n                                                expressionString.length() - 1);\n                            }\n                            sortProperty = expressionString.substring(var.length() + 1);\n\n                            actualComparator = new BeanComparator(\n                                    sortProperty, comparator, !sheetState.isAscending());\n\n                            if (LOG.isDebugEnabled()) {\n                                LOG.debug(\"Sort property is {}\", sortProperty);\n                            }\n                        } else {\n\n                            final boolean descending = !sheetState.isAscending();\n                            final ValueExpression expression = child.getValueExpression(\"value\");\n                            actualComparator = new ValueExpressionComparator(facesContext, var, expression, descending, comparator);\n                        }\n                    } else {\n                        LOG.error(\"No sorting performed. No Expression target found for sorting!\");\n                        unsetSortableAttribute(column);\n                        return;\n                    }\n                } else {\n                    LOG.error(\"No sorting performed. Value is not instanceof List or Object[]!\");\n                    unsetSortableAttribute(column);\n                    return;\n                }\n            } catch (final Exception e) {\n                LOG.error(\"Error while extracting sortMethod :\" + e.getMessage(), e);\n                if (column != null) {\n                    unsetSortableAttribute(column);\n                }\n                return;\n            }\n\n            // TODO: locale / comparator parameter?\n            // don't compare numbers with Collator.getInstance() comparator\n//        Comparator comparator = Collator.getInstance();\n//          comparator = new RowComparator(ascending, method);\n\n            // memorize selected rows\n            List<Object> selectedDataRows = null;\n            if (sheetState.getSelectedRows().size() > 0) {\n                selectedDataRows = new ArrayList<Object>(sheetState.getSelectedRows().size());\n                Object dataRow;\n                for (final Integer index : sheetState.getSelectedRows()) {\n                    if (value instanceof List) {\n                        dataRow = ((List) value).get(index);\n                    } else {\n                        dataRow = ((Object[]) value)[index];\n                    }\n                    selectedDataRows.add(dataRow);\n                }\n            }\n\n            // do sorting\n            if (value instanceof List) {\n                Collections.sort((List) value, actualComparator);\n            } else { // value is instanceof Object[]\n                Arrays.sort((Object[]) value, actualComparator);\n            }\n\n            // restore selected rows\n            if (selectedDataRows != null) {\n                sheetState.getSelectedRows().clear();\n                for (final Object dataRow : selectedDataRows) {\n                    int index = -1;\n                    if (value instanceof List) {\n                        for (int i = 0; i < ((List) value).size() && index < 0; i++) {\n                            if (dataRow == ((List) value).get(i)) {\n                                index = i;\n                            }\n                        }\n                    } else {\n                        for (int i = 0; i < ((Object[]) value).length && index < 0; i++) {\n                            if (dataRow == ((Object[]) value)[i]) {\n                                index = i;\n                            }\n                        }\n                    }\n                    if (index >= 0) {\n                        sheetState.getSelectedRows().add(index);\n                    }\n                }\n            }\n\n        } else {  // DataModel?, ResultSet, Result or Object\n            LOG.warn(\"Sorting not supported for type \"\n                    + (value != null ? value.getClass().toString() : \"null\"));\n        }\n    }\n\n    // XXX needs to be tested\n    // XXX was based on ^#\\{(\\w+(\\.\\w)*)\\}$ which is wrong, because there is a + missing after the last \\w\n    boolean isSimpleProperty(final String expressionString) {\n        if (expressionString.startsWith(\"#{\") && expressionString.endsWith(\"}\")) {\n            final String inner = expressionString.substring(2, expressionString.length() - 1);\n            final String[] parts = StringUtils.split(inner, '.');\n            for (final String part : parts) {\n                if (!StringUtils.isAlpha(part)) {\n                    return false;\n                }\n            }\n            return true;\n        }\n        return false;\n    }\n\n    private void unsetSortableAttribute(final UIColumn uiColumn) {\n        LOG.warn(\"removing attribute sortable from column \" + uiColumn.getId());\n        uiColumn.getAttributes().put(Attributes.SORTABLE, Boolean.FALSE);\n    }\n\n    private UIComponent getFirstSortableChild(final List<UIComponent> children) {\n        UIComponent result = null;\n\n        for (UIComponent child : children) {\n            result = child;\n            if (child instanceof UISelectMany\n                    || child instanceof UISelectOne\n                    || child instanceof UISelectBoolean\n                    || (child instanceof AbstractUICommand && child.getChildren().isEmpty())\n                    || (child instanceof UIInput && RendererTypes.HIDDEN.equals(child.getRendererType()))) {\n                continue;\n                // look for a better component if any\n            }\n            if (child instanceof UIOutput) {\n                break;\n            }\n            if (child instanceof UICommand\n                    || child instanceof javax.faces.component.UIPanel) {\n                child = getFirstSortableChild(child.getChildren());\n                if (child instanceof UIOutput) {\n                    break;\n                }\n            }\n        }\n        return result;\n    }\n\n    public Comparator getComparator() {\n        return comparator;\n    }\n\n    public void setComparator(final Comparator comparator) {\n        this.comparator = comparator;\n    }\n}\n"
  },
  {
    "path": "test-resources/src/main/resources/AttributeHandlerJavaEleven.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.myfaces.tobago.facelets;\n\nimport org.apache.commons.beanutils.PropertyUtils;\nimport org.apache.myfaces.tobago.component.Attributes;\nimport org.apache.myfaces.tobago.component.SupportsMarkup;\nimport org.apache.myfaces.tobago.component.SupportsRenderedPartially;\nimport org.apache.myfaces.tobago.context.Markup;\nimport org.apache.myfaces.tobago.el.ConstantMethodBinding;\nimport org.apache.myfaces.tobago.internal.util.StringUtils;\nimport org.apache.myfaces.tobago.util.ComponentUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ELException;\nimport javax.el.ExpressionFactory;\nimport javax.el.MethodExpression;\nimport javax.el.ValueExpression;\nimport javax.faces.FacesException;\nimport javax.faces.component.ActionSource;\nimport javax.faces.component.ActionSource2;\nimport javax.faces.component.EditableValueHolder;\nimport javax.faces.component.UIComponent;\nimport javax.faces.component.ValueHolder;\nimport javax.faces.convert.Converter;\nimport javax.faces.event.MethodExpressionActionListener;\nimport javax.faces.event.MethodExpressionValueChangeListener;\nimport javax.faces.validator.MethodExpressionValidator;\nimport javax.faces.view.facelets.ComponentHandler;\nimport javax.faces.view.facelets.FaceletContext;\nimport javax.faces.view.facelets.TagAttribute;\nimport javax.faces.view.facelets.TagConfig;\nimport javax.faces.view.facelets.TagException;\nimport javax.faces.view.facelets.TagHandler;\nimport java.beans.IntrospectionException;\nimport java.beans.PropertyDescriptor;\n\n//from Apache MyFaces 2.0.8\n//Retrieved from http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/facelets/AttributeHandler.java/?v=source\npublic final class AttributeHandlerJavaEleven extends TagHandler {\n\n    private static final Logger LOG = LoggerFactory.getLogger(AttributeHandlerJavaEleven.class);\n\n    private final TagAttribute name;\n\n    private final TagAttribute value;\n\n    private final TagAttribute mode;\n\n    public AttributeHandler(final TagConfig config) {\n        super(config);\n        this.name = getRequiredAttribute(Attributes.NAME);\n        this.value = getRequiredAttribute(Attributes.VALUE);\n        this.mode = getAttribute(Attributes.MODE);\n    }\n\n    public void apply(final FaceletContext faceletContext, final UIComponent parent) throws ELException {\n        if (parent == null) {\n            throw new TagException(tag, \"Parent UIComponent was null\");\n        }\n\n        if (ComponentHandler.isNew(parent)) {\n\n            if (mode != null) {\n                if (\"isNotSet\".equals(mode.getValue())) {\n                    boolean result = false;\n                    var expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = true;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = false;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"isSet\".equals(mode.getValue())) {\n                    boolean result = true;\n                    var expressionString = value.getValue();\n                    if (!value.isLiteral()) {\n                        while (isSimpleExpression(expressionString)) {\n                            if (isMethodOrValueExpression(expressionString)) {\n                                final ValueExpression expression\n                                        = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                                if (expression == null) {\n                                    result = false;\n                                    break;\n                                } else {\n                                    expressionString = expression.getExpressionString();\n                                }\n                            } else {\n                                result = true;\n                                break;\n                            }\n                        }\n                    } else {\n                        result = StringUtils.isNotEmpty(expressionString);\n                    }\n                    parent.getAttributes().put(name.getValue(), result);\n                } else if (\"action\".equals(mode.getValue())) {\n                    var expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                // when the action hasn't been set while using a composition.\n                                if (LOG.isDebugEnabled()) {\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression action = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, String.class, ComponentUtils.ACTION_ARGS));\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (\"actionListener\".equals(mode.getValue())) {\n                    var expressionString = value.getValue();\n                    while (isSimpleExpression(expressionString)) {\n                        if (isMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression\n                                    = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                            if (expression == null) {\n                                if (LOG.isDebugEnabled()) {\n                                    // when the action hasn't been set while using a composition.\n                                    LOG.debug(\"Variable can't be resolved: value='\" + expressionString + \"'\");\n                                }\n                                expressionString = null;\n                                break;\n                            } else {\n                                expressionString = expression.getExpressionString();\n                            }\n                        } else {\n                            LOG.warn(\"Only expressions are supported mode=actionListener value='\" + expressionString + \"'\");\n                            expressionString = null;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                        final MethodExpression actionListener\n                                = new TagMethodExpression(value, expressionFactory.createMethodExpression(\n                                faceletContext, expressionString, null, ComponentUtils.ACTION_LISTENER_ARGS));\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(actionListener));\n                    }\n                } else if (\"actionFromValue\".equals(mode.getValue())) {\n                    if (!value.isLiteral()) {\n                        final String result = value.getValue(faceletContext);\n                        parent.getAttributes().put(name.getValue(), new ConstantMethodBinding(result));\n                    }\n                } else if (\"valueIfSet\".equals(mode.getValue())) {\n                    var expressionString = value.getValue();\n                    String lastExpressionString = null;\n                    while (isMethodOrValueExpression(expressionString) && isSimpleExpression(expressionString)) {\n                        final ValueExpression expression\n                                = faceletContext.getVariableMapper().resolveVariable(removeElParenthesis(expressionString));\n                        if (expression != null) {\n                            lastExpressionString = expressionString;\n                            expressionString = expression.getExpressionString();\n                        } else {\n                            // restore last value\n                            expressionString = lastExpressionString;\n                            break;\n                        }\n                    }\n                    if (expressionString != null) {\n                        final String attributeName = name.getValue(faceletContext);\n                        if (containsMethodOrValueExpression(expressionString)) {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(attributeName, expression);\n                        } else {\n                            final Object literalValue = getValue(faceletContext, parent, expressionString, attributeName);\n                            parent.getAttributes().put(attributeName, literalValue);\n                        }\n                    }\n                } else {\n                    throw new FacesException(\"Type \" + mode + \" not supported\");\n                }\n            } else {\n\n                final String nameValue = name.getValue(faceletContext);\n                if (Attributes.RENDERED.equals(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.setRendered(value.getBoolean(faceletContext));\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Boolean.class));\n                    }\n                } else if (Attributes.RENDERED_PARTIALLY.equals(nameValue)\n                        && parent instanceof SupportsRenderedPartially) {\n\n                    if (value.isLiteral()) {\n                        final String[] components = ComponentUtils.splitList(value.getValue());\n                        ((SupportsRenderedPartially) parent).setRenderedPartially(components);\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                } else if (Attributes.STYLE_CLASS.equals(nameValue)) {\n                    // TODO expression\n                    ComponentUtils.setStyleClasses(parent, value.getValue());\n                } else if (Attributes.MARKUP.equals(nameValue)) {\n                    if (parent instanceof SupportsMarkup) {\n                        if (value.isLiteral()) {\n                            ((SupportsMarkup) parent).setMarkup(Markup.valueOf(value.getValue()));\n                        } else {\n                            final ValueExpression expression = value.getValueExpression(faceletContext, Object.class);\n                            parent.setValueExpression(nameValue, expression);\n                        }\n                    } else {\n                        LOG.error(\"Component is not instanceof SupportsMarkup. Instance is: \" + parent.getClass().getName());\n                    }\n                } else if (parent instanceof EditableValueHolder && Attributes.VALIDATOR.equals(nameValue)) {\n                    final MethodExpression methodExpression\n                            = getMethodExpression(faceletContext, null, ComponentUtils.VALIDATOR_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValidator(new MethodExpressionValidator(methodExpression));\n                    }\n                } else if (parent instanceof EditableValueHolder\n                        && Attributes.VALUE_CHANGE_LISTENER.equals(nameValue)) {\n                    final MethodExpression methodExpression =\n                            getMethodExpression(faceletContext, null, ComponentUtils.VALUE_CHANGE_LISTENER_ARGS);\n                    if (methodExpression != null) {\n                        ((EditableValueHolder) parent).addValueChangeListener(\n                                new MethodExpressionValueChangeListener(methodExpression));\n                    }\n                } else if (parent instanceof ValueHolder && Attributes.CONVERTER.equals(nameValue)) {\n                    setConverter(faceletContext, parent, nameValue);\n                } else if (parent instanceof ActionSource && Attributes.ACTION.equals(nameValue)) {\n                    final MethodExpression action = getMethodExpression(faceletContext, String.class, ComponentUtils.ACTION_ARGS);\n                    if (action != null) {\n                        ((ActionSource2) parent).setActionExpression(action);\n                    }\n                } else if (parent instanceof ActionSource && Attributes.ACTION_LISTENER.equals(nameValue)) {\n                    final MethodExpression action\n                            = getMethodExpression(faceletContext, null, ComponentUtils.ACTION_LISTENER_ARGS);\n                    if (action != null) {\n                        ((ActionSource) parent).addActionListener(new MethodExpressionActionListener(action));\n                    }\n                } else if (!parent.getAttributes().containsKey(nameValue)) {\n                    if (value.isLiteral()) {\n                        parent.getAttributes().put(nameValue, value.getValue());\n                    } else {\n                        parent.setValueExpression(nameValue, value.getValueExpression(faceletContext, Object.class));\n                    }\n                }\n            }\n        }\n    }\n\n    private boolean isMethodOrValueExpression(final String string) {\n        return (string.startsWith(\"${\") || string.startsWith(\"#{\")) && string.endsWith(\"}\");\n    }\n\n    private boolean containsMethodOrValueExpression(final String string) {\n        return (string.contains(\"${\") || string.contains(\"#{\")) && string.contains(\"}\");\n    }\n\n    private boolean isSimpleExpression(final String string) {\n        return string.indexOf('.') < 0 && string.indexOf('[') < 0;\n    }\n\n    private String removeElParenthesis(final String string) {\n        return string.substring(2, string.length() - 1);\n    }\n\n    private ValueExpression getExpression(final FaceletContext faceletContext) {\n        final String myValue = removeElParenthesis(value.getValue());\n        return faceletContext.getVariableMapper().resolveVariable(myValue);\n    }\n\n    private MethodExpression getMethodExpression(\n            final FaceletContext faceletContext, final Class returnType, final Class[] args) {\n        // in a composition may be we get the method expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n                return new TagMethodExpression(value, expressionFactory.createMethodExpression(faceletContext,\n                        expression.getExpressionString(), returnType, args));\n            } else {\n                return null;\n            }\n        } else {\n            return value.getMethodExpression(faceletContext, returnType, args);\n        }\n    }\n\n    private Object getValue(\n            final FaceletContext faceletContext, final UIComponent parent, final String expressionString,\n            final String attributeName) {\n        Class type = Object.class;\n        try {\n            type = PropertyUtils.getReadMethod(\n                    new PropertyDescriptor(attributeName, parent.getClass())).getReturnType();\n        } catch (final IntrospectionException e) {\n            LOG.warn(\"Can't determine expected type\", e);\n        }\n        final ExpressionFactory expressionFactory = faceletContext.getExpressionFactory();\n        final ValueExpression valueExpression = expressionFactory\n                .createValueExpression(faceletContext, expressionString, type);\n        return valueExpression.getValue(faceletContext);\n    }\n\n    private void setConverter(final FaceletContext faceletContext, final UIComponent parent, final String nameValue) {\n        // in a composition may be we get the converter expression string from the current variable mapper\n        // the expression can be empty\n        // in this case return nothing\n        if (value.getValue().startsWith(\"${\")) {\n            final ValueExpression expression = getExpression(faceletContext);\n            if (expression != null) {\n                setConverter(faceletContext, parent, nameValue, expression);\n            }\n        } else {\n            setConverter(faceletContext, parent, nameValue, value.getValueExpression(faceletContext, Object.class));\n        }\n    }\n\n    private void setConverter(\n            final FaceletContext faceletContext, final UIComponent parent, final String nameValue,\n            final ValueExpression expression) {\n        if (expression.isLiteralText()) {\n            final Converter converter =\n                    faceletContext.getFacesContext().getApplication().createConverter(expression.getExpressionString());\n            ((ValueHolder) parent).setConverter(converter);\n        } else {\n            parent.setValueExpression(nameValue, expression);\n        }\n    }\n}"
  },
  {
    "path": "test-resources/src/main/resources/Attributes.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.myfaces.tobago.component;\n\n/**\n * Name constants of the attributes of the Tobago components.\n */\n//from http://grepcode.com/file_/repo1.maven.org/maven2/org.apache.myfaces.tobago/tobago-core/2.0.8/org/apache/myfaces/tobago/component/Attributes.java/?v=source\npublic final class Attributes {\n\n    public static final String ACCESS_KEY = \"accessKey\";\n    public static final String ACTION = \"action\";\n    public static final String ACTION_LISTENER = \"actionListener\";\n    public static final String ALIGN = \"align\";\n    public static final String ALT = \"alt\";\n    public static final String APPLICATION_ICON = \"applicationIcon\";\n    public static final String AUTO_RELOAD = \"autoReload\";\n    public static final String BODY_CONTENT = \"bodyContent\";\n    public static final String BORDER = \"border\";\n    /** Used by a layout manager */\n    public static final String BORDER_BOTTOM = \"borderBottom\";\n    /** Used by a layout manager */\n    public static final String BORDER_LEFT = \"borderLeft\";\n    /** Used by a layout manager */\n    public static final String BORDER_RIGHT = \"borderRight\";\n    /** Used by a layout manager */\n    public static final String BORDER_TOP = \"borderTop\";\n    public static final String CHARSET = \"charset\";\n    /** @deprecated */\n    @Deprecated\n    public static final String CELLSPACING = \"cellspacing\";\n    public static final String CLIENT_PROPERTIES = \"clientProperties\";\n    public static final String COLUMN_SPAN = \"columnSpan\";\n    public static final String COLUMN_SPACING = \"columnSpacing\";\n    public static final String COLUMNS = \"columns\";\n    public static final String CONVERTER = \"converter\";\n    public static final String CREATE_SPAN = \"createSpan\";\n    public static final String CSS_CLASSES_BLOCKS = \"cssClassesBlocks\";\n    /** @deprecated since 2.0.0 */\n    @Deprecated\n    public static final String DATE_INPUT_ID = \"dateInputId\";\n    public static final String DATE_STYLE = \"dateStyle\";\n    public static final String DEFAULT_COMMAND = \"defaultCommand\";\n    public static final String DELAY = \"delay\";\n    public static final String DIRECT_LINK_COUNT = \"directLinkCount\";\n    public static final String DISABLED = \"disabled\";\n    public static final String ENCTYPE = \"enctype\";\n    public static final String ESCAPE = \"escape\";\n    public static final String EXPANDED = \"expanded\";\n    public static final String EXECUTE = \"execute\";\n    public static final String EVENT = \"event\";\n    public static final String FIELD_ID = \"fieldId\";\n    public static final String FIRST = \"first\";\n    public static final String FREQUENCY = \"frequency\";\n    public static final String FOCUS = \"focus\";\n    public static final String FOCUS_ID = \"focusId\";\n    public static final String FORCE_VERTICAL_SCROLLBAR = \"forceVerticalScrollbar\";\n    public static final String FORMAT_PATTERN = \"formatPattern\";\n    public static final String FOR = \"for\";\n    public static final String GLOBAL_ONLY = \"globalOnly\";\n    public static final String HEIGHT = \"height\";\n    public static final String HIDDEN = \"hidden\";\n    public static final String HOVER = \"hover\";\n    public static final String I18N = \"i18n\";\n    public static final String ICON_SIZE = \"iconSize\";\n    public static final String ID = \"id\";\n    public static final String IMMEDIATE = \"immediate\";\n    public static final String IMAGE = \"image\";\n    public static final String INLINE = \"inline\";\n    /** @deprecated */\n    @Deprecated\n    public static final String INNER_HEIGHT = \"innerHeight\";\n    /** @deprecated */\n    @Deprecated\n    public static final String INNER_WIDTH = \"innerWidth\";\n    public static final String ITEM_DESCRIPTION = \"itemDescription\";\n    public static final String ITEM_DISABLED = \"itemDisabled\";\n    public static final String ITEM_LABEL = \"itemLabel\";\n    public static final String ITEM_IMAGE = \"itemImage\";\n    public static final String ITEM_VALUE = \"itemValue\";\n    public static final String JSF_RESOURCE = \"jsfResource\";\n    public static final String LABEL = \"label\";\n    public static final String LABEL_POSITION = \"labelPosition\";\n    public static final String LABEL_WIDTH = \"labelWidth\";\n    public static final String LAYOUT_HEIGHT = \"layoutHeight\";\n    public static final String LAYOUT_ORDER = \"layoutOrder\";\n    public static final String LAYOUT_WIDTH = \"layoutWidth\";\n    public static final String LEFT = \"left\";\n    public static final String LINK = \"link\";\n    /** @deprecated */\n    @Deprecated\n    public static final String MARGIN = \"margin\";\n    /** Used by a layout manager */\n    public static final String MARGIN_BOTTOM = \"marginBottom\";\n    /** Used by a layout manager */\n    public static final String MARGIN_LEFT = \"marginLeft\";\n    /** Used by a layout manager */\n    public static final String MARGIN_RIGHT = \"marginRight\";\n    /** Used by a layout manager */\n    public static final String MARGIN_TOP = \"marginTop\";\n    public static final String MARKED = \"marked\";\n    public static final String MARKUP = \"markup\";\n    public static final String MAX = \"max\";\n    public static final String MAX_SEVERITY = \"maxSeverity\";\n    public static final String MAX_NUMBER = \"maxNumber\";\n    public static final String MAXIMUM_HEIGHT = \"maximumHeight\";\n    public static final String MAXIMUM_WIDTH = \"maximumWidth\";\n    public static final String METHOD = \"method\";\n    public static final String MIN = \"min\";\n    public static final String MIN_SEVERITY = \"minSeverity\";\n    public static final String MINIMUM_HEIGHT = \"minimumHeight\";\n    public static final String MINIMUM_WIDTH = \"minimumWidth\";\n    public static final String MODAL = \"modal\";\n    public static final String MODE = \"mode\";\n    public static final String MUTABLE = \"mutable\";\n    public static final String NAME = \"name\";\n    public static final String NAVIGATE = \"navigate\";\n    public static final String NUMBER_STYLE = \"numberStyle\";\n    public static final String OMIT = \"omit\";\n    /** @deprecated Since 2.0.0. This attribute work not with SCP */\n    @Deprecated\n    public static final String ONCHANGE = \"onchange\";\n    /** @deprecated Since 2.0.0. This attribute work not with SCP */\n    @Deprecated\n    public static final String ONCLICK = \"onclick\";\n    public static final String ORDER_BY = \"orderBy\";\n    public static final String ORIENTATION = \"orientation\";\n    /** Used by a layout manager */\n    public static final String PADDING_BOTTOM = \"paddingBottom\";\n    /** Used by a layout manager */\n    public static final String PADDING_LEFT = \"paddingLeft\";\n    /** Used by a layout manager */\n    public static final String PADDING_RIGHT = \"paddingRight\";\n    /** Used by a layout manager */\n    public static final String PADDING_TOP = \"paddingTop\";\n    /** @deprecated Since 2.0.6. No longer needed. */\n    @Deprecated\n    public static final String PAGE_MENU = \"pageMenu\";\n    public static final String PASSWORD = \"password\";\n    public static final String POPUP_CLOSE = \"popupClose\";\n    public static final String POPUP_LIST = \"popupList\";\n    public static final String POPUP_RESET = \"popupReset\";\n    public static final String POPUP_CALENDAR_ID = \"popupCalendarId\";\n    public static final String PREFERRED_HEIGHT = \"preferredHeight\";\n    public static final String PREFERRED_WIDTH = \"preferredWidth\";\n    public static final String PREFORMATED = \"preformated\";\n    public static final String READONLY = \"readonly\";\n    public static final String REFERENCE = \"reference\";\n    public static final String RELATIVE = \"relative\";\n    public static final String RENDERED = \"rendered\";\n    public static final String RENDERED_PARTIALLY = \"renderedPartially\";\n    public static final String RENDERER_TYPE = \"rendererType\";\n    public static final String RENDER_AS = \"renderAs\";\n    public static final String RENDER_RANGE = \"renderRange\";\n    public static final String RENDER_RANGE_EXTERN = \"renderRangeExtern\";\n    public static final String REQUIRED = \"required\";\n    public static final String RESIZABLE = \"resizable\";\n    public static final String RESOURCE = \"resource\";\n    public static final String ROW_ID = \"rowId\";\n    public static final String ROW_SPAN = \"rowSpan\";\n    public static final String ROW_SPACING = \"rowSpacing\";\n    public static final String ROWS = \"rows\";\n    public static final String SCRIPT_FILES = \"scriptFiles\";\n    public static final String SCROLLBAR_HEIGHT = \"scrollbarHeight\";\n    public static final String SCROLLBARS = \"scrollbars\";\n    // Attribute name could not be the same as the method name\n    // this cause an infinite loop on attribute map\n    public static final String SCROLL_POSITION = \"attrScrollPosition\";\n    public static final String SELECTED_INDEX = \"selectedIndex\";\n    public static final String SELECTED_LIST_STRING = \"selectedListString\";\n    public static final String SORTABLE = \"sortable\";\n    public static final String SELECTABLE = \"selectable\";\n    public static final String SHOW_DIRECT_LINKS = \"showDirectLinks\";\n    public static final String SHOW_HEADER = \"showHeader\";\n    public static final String SHOW_JUNCTIONS = \"showJunctions\";\n    public static final String SHOW_NAVIGATION_BAR = \"showNavigationBar\";\n    public static final String SHOW_PAGE_RANGE = \"showPageRange\";\n    public static final String SHOW_ROOT = \"showRoot\";\n    public static final String SHOW_ROOT_JUNCTION = \"showRootJunction\";\n    public static final String SHOW_ROW_RANGE = \"showRowRange\";\n    public static final String SHOW_SUMMARY = \"showSummary\";\n    public static final String SHOW_DETAIL = \"showDetail\";\n    public static final String SPAN_X = \"spanX\";\n    public static final String SPAN_Y = \"spanY\";\n    public static final String SRC = \"src\";\n    public static final String STATE = \"state\";\n    public static final String STATE_PREVIEW = \"statePreview\";\n    public static final String STYLE = \"style\";\n    /** @deprecated */\n    @Deprecated\n    public static final String STYLE_CLASS = \"styleClass\";\n    public static final String SUPPRESS_TOOLBAR_CONTAINER = \"suppressToolbarContainer\";\n    public static final String SWITCH_TYPE = \"switchType\";\n    public static final String TAB_INDEX = \"tabIndex\";\n    public static final String TARGET = \"target\";\n    public static final String TIME_STYLE = \"timeStyle\";\n    public static final String TEXT_ALIGN = \"textAlign\";\n    public static final String TIMEZONE = \"timezone\";\n    public static final String TITLE = \"title\";\n    public static final String TIP = \"tip\";\n    public static final String TOP = \"top\";\n    public static final String TRANSITION = \"transition\";\n    public static final String TYPE = \"type\";\n    public static final String VALUE = \"value\";\n    public static final String VALUE_CHANGE_LISTENER = \"valueChangeListener\";\n    public static final String VAR = \"var\";\n    public static final String UNIT = \"unit\";\n    public static final String UPDATE = \"update\";\n    public static final String VALIDATOR = \"validator\";\n    public static final String WIDTH = \"width\";\n    public static final String WIDTH_LIST = \"widthList\";\n    public static final String WIDTH_LIST_STRING = \"widthListString\";\n    public static final String Z_INDEX = \"zIndex\";\n}"
  },
  {
    "path": "test-resources/src/main/resources/Console.java",
    "content": "/*\n * Copyright 2018 Confluent Inc.\n *\n * Licensed under the Confluent Community License (the \"License\"); you may not use\n * this file except in compliance with the License.  You may obtain a copy of the\n * License at\n *\n * http://www.confluent.io/confluent-community-license\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n * WARRANTIES OF ANY KIND, either express or implied.  See the License for the\n * specific language governing permissions and limitations under the License.\n */\n\npackage io.confluent.ksql.cli.console;\n\nimport static io.confluent.ksql.util.CmdLineUtil.splitByUnquotedWhitespace;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.common.collect.ImmutableList;\nimport com.google.common.collect.ImmutableMap;\nimport com.google.common.collect.Maps;\nimport com.google.common.collect.Streams;\nimport io.confluent.ksql.cli.console.CliConfig.OnOff;\nimport io.confluent.ksql.cli.console.KsqlTerminal.HistoryEntry;\nimport io.confluent.ksql.cli.console.KsqlTerminal.StatusClosable;\nimport io.confluent.ksql.cli.console.cmd.CliSpecificCommand;\nimport io.confluent.ksql.cli.console.table.Table;\nimport io.confluent.ksql.cli.console.table.Table.Builder;\nimport io.confluent.ksql.cli.console.table.builder.CommandStatusTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.ConnectorInfoTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.ConnectorListTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.ConnectorPluginsListTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.DropConnectorTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.ExecutionPlanTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.FunctionNameListTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.KafkaTopicsListTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.ListVariablesTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.PropertiesListTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.QueriesTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.StreamsListTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.TableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.TablesListTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.TerminateQueryTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.TopicDescriptionTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.TypeListTableBuilder;\nimport io.confluent.ksql.cli.console.table.builder.WarningEntityTableBuilder;\nimport io.confluent.ksql.metrics.TopicSensors.Stat;\nimport io.confluent.ksql.model.WindowType;\nimport io.confluent.ksql.query.QueryError;\nimport io.confluent.ksql.rest.ApiJsonMapper;\nimport io.confluent.ksql.rest.entity.ArgumentInfo;\nimport io.confluent.ksql.rest.entity.AssertSchemaEntity;\nimport io.confluent.ksql.rest.entity.AssertTopicEntity;\nimport io.confluent.ksql.rest.entity.CommandStatusEntity;\nimport io.confluent.ksql.rest.entity.ConnectorDescription;\nimport io.confluent.ksql.rest.entity.ConnectorList;\nimport io.confluent.ksql.rest.entity.ConnectorPluginsList;\nimport io.confluent.ksql.rest.entity.CreateConnectorEntity;\nimport io.confluent.ksql.rest.entity.DropConnectorEntity;\nimport io.confluent.ksql.rest.entity.ExecutionPlan;\nimport io.confluent.ksql.rest.entity.FieldInfo;\nimport io.confluent.ksql.rest.entity.FieldInfo.FieldType;\nimport io.confluent.ksql.rest.entity.FunctionDescriptionList;\nimport io.confluent.ksql.rest.entity.FunctionInfo;\nimport io.confluent.ksql.rest.entity.FunctionNameList;\nimport io.confluent.ksql.rest.entity.KafkaTopicsList;\nimport io.confluent.ksql.rest.entity.KafkaTopicsListExtended;\nimport io.confluent.ksql.rest.entity.KsqlEntity;\nimport io.confluent.ksql.rest.entity.KsqlErrorMessage;\nimport io.confluent.ksql.rest.entity.KsqlStatementErrorMessage;\nimport io.confluent.ksql.rest.entity.KsqlWarning;\nimport io.confluent.ksql.rest.entity.PropertiesList;\nimport io.confluent.ksql.rest.entity.Queries;\nimport io.confluent.ksql.rest.entity.QueryDescription;\nimport io.confluent.ksql.rest.entity.QueryDescriptionEntity;\nimport io.confluent.ksql.rest.entity.QueryDescriptionList;\nimport io.confluent.ksql.rest.entity.QueryHostStat;\nimport io.confluent.ksql.rest.entity.QueryOffsetSummary;\nimport io.confluent.ksql.rest.entity.QueryTopicOffsetSummary;\nimport io.confluent.ksql.rest.entity.RunningQuery;\nimport io.confluent.ksql.rest.entity.SourceDescription;\nimport io.confluent.ksql.rest.entity.SourceDescriptionEntity;\nimport io.confluent.ksql.rest.entity.SourceDescriptionList;\nimport io.confluent.ksql.rest.entity.StreamedRow;\nimport io.confluent.ksql.rest.entity.StreamedRow.DataRow;\nimport io.confluent.ksql.rest.entity.StreamedRow.Header;\nimport io.confluent.ksql.rest.entity.StreamsList;\nimport io.confluent.ksql.rest.entity.TablesList;\nimport io.confluent.ksql.rest.entity.TerminateQueryEntity;\nimport io.confluent.ksql.rest.entity.TopicDescription;\nimport io.confluent.ksql.rest.entity.TypeList;\nimport io.confluent.ksql.rest.entity.VariablesList;\nimport io.confluent.ksql.rest.entity.WarningEntity;\nimport io.confluent.ksql.util.CmdLineUtil;\nimport io.confluent.ksql.util.HandlerMaps;\nimport io.confluent.ksql.util.HandlerMaps.ClassHandlerMap1;\nimport io.confluent.ksql.util.HandlerMaps.Handler1;\nimport io.confluent.ksql.util.KsqlException;\nimport io.confluent.ksql.util.TabularRow;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.nio.charset.Charset;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Instant;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.StringTokenizer;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport org.apache.commons.lang3.ObjectUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.kafka.common.config.ConfigException;\nimport org.apache.kafka.connect.runtime.rest.entities.ConnectorStateInfo;\nimport org.jline.terminal.Terminal.Signal;\nimport org.jline.terminal.Terminal.SignalHandler;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n// CHECKSTYLE_RULES.OFF: ClassDataAbstractionCoupling\npublic class Console implements Closeable {\n    // CHECKSTYLE_RULES.ON: ClassDataAbstractionCoupling\n\n    private static final Logger log = LoggerFactory.getLogger(Console.class);\n    private static final ObjectMapper OBJECT_MAPPER = ApiJsonMapper.INSTANCE.get();\n\n    private static final ClassHandlerMap1<KsqlEntity, Console> PRINT_HANDLERS =\n            HandlerMaps.forClass(KsqlEntity.class).withArgType(Console.class)\n                    .put(CommandStatusEntity.class,\n                            tablePrinter(CommandStatusEntity.class, CommandStatusTableBuilder::new))\n                    .put(PropertiesList.class,\n                            tablePrinter(PropertiesList.class, PropertiesListTableBuilder::new))\n                    .put(Queries.class,\n                            tablePrinter(Queries.class, QueriesTableBuilder::new))\n                    .put(SourceDescriptionEntity.class,\n                            (console, entity) -> console.printSourceDescription(entity.getSourceDescription()))\n                    .put(SourceDescriptionList.class,\n                            Console::printSourceDescriptionList)\n                    .put(QueryDescriptionEntity.class,\n                            (console, entity) -> console.printQueryDescription(entity.getQueryDescription()))\n                    .put(QueryDescriptionList.class,\n                            Console::printQueryDescriptionList)\n                    .put(TopicDescription.class,\n                            tablePrinter(TopicDescription.class, TopicDescriptionTableBuilder::new))\n                    .put(StreamsList.class,\n                            tablePrinter(StreamsList.class, StreamsListTableBuilder::new))\n                    .put(TablesList.class,\n                            tablePrinter(TablesList.class, TablesListTableBuilder::new))\n                    .put(KafkaTopicsList.class,\n                            tablePrinter(KafkaTopicsList.class, KafkaTopicsListTableBuilder.SimpleBuilder::new))\n                    .put(KafkaTopicsListExtended.class,\n                            tablePrinter(\n                                    KafkaTopicsListExtended.class,\n                                    KafkaTopicsListTableBuilder.ExtendedBuilder::new))\n                    .put(ExecutionPlan.class,\n                            tablePrinter(ExecutionPlan.class, ExecutionPlanTableBuilder::new))\n                    .put(FunctionNameList.class,\n                            tablePrinter(FunctionNameList.class, FunctionNameListTableBuilder::new))\n                    .put(FunctionDescriptionList.class,\n                            Console::printFunctionDescription)\n                    .put(CreateConnectorEntity.class,\n                            tablePrinter(CreateConnectorEntity.class, ConnectorInfoTableBuilder::new))\n                    .put(DropConnectorEntity.class,\n                            tablePrinter(DropConnectorEntity.class, DropConnectorTableBuilder::new))\n                    .put(ConnectorList.class,\n                            tablePrinter(ConnectorList.class, ConnectorListTableBuilder::new))\n                    .put(ConnectorPluginsList.class,\n                            tablePrinter(ConnectorPluginsList.class, ConnectorPluginsListTableBuilder::new))\n                    .put(ConnectorDescription.class,\n                            Console::printConnectorDescription)\n                    .put(TypeList.class,\n                            tablePrinter(TypeList.class, TypeListTableBuilder::new))\n                    .put(WarningEntity.class,\n                            tablePrinter(WarningEntity.class, WarningEntityTableBuilder::new))\n                    .put(VariablesList.class,\n                            tablePrinter(VariablesList.class, ListVariablesTableBuilder::new))\n                    .put(TerminateQueryEntity.class,\n                            tablePrinter(TerminateQueryEntity.class, TerminateQueryTableBuilder::new))\n                    .put(AssertTopicEntity.class, Console::printAssertTopic)\n                    .put(AssertSchemaEntity.class, Console::printAssertSchema)\n                    .build();\n\n    private static <T extends KsqlEntity> Handler1<KsqlEntity, Console> tablePrinter(\n            final Class<T> entityType,\n            final Supplier<? extends TableBuilder<T>> tableBuilderType) {\n\n        try {\n            final TableBuilder<T> tableBuilder = tableBuilderType.get();\n\n            return (console, type) -> {\n                final Table table = tableBuilder.buildTable(entityType.cast(type));\n                table.print(console);\n            };\n        } catch (final Exception e) {\n            throw new IllegalStateException(\"Error instantiating tableBuilder: \" + tableBuilderType);\n        }\n    }\n\n    private final Map<String, CliSpecificCommand> cliSpecificCommands;\n    private final KsqlTerminal terminal;\n    private final RowCaptor rowCaptor;\n    private OutputFormat outputFormat;\n    private Optional<File> spoolFile = Optional.empty();\n    private CliConfig config;\n\n    public interface RowCaptor {\n\n        void addRow(DataRow row);\n\n        void addRows(List<List<String>> fields);\n    }\n\n    public static Console build(final OutputFormat outputFormat) {\n        final AtomicReference<Console> consoleRef = new AtomicReference<>();\n        final Predicate<String> isCliCommand = line -> {\n            final Console theConsole = consoleRef.get();\n            return theConsole != null && theConsole.getCliCommand(line).isPresent();\n        };\n\n        final Path historyFilePath = Paths.get(System.getProperty(\n                \"history-file\",\n                System.getProperty(\"user.home\")\n                        + \"/.ksql-history\"\n        )).toAbsolutePath();\n\n        final KsqlTerminal terminal = new JLineTerminal(isCliCommand, historyFilePath);\n\n        final Console console = new Console(\n                outputFormat, terminal, new NoOpRowCaptor());\n\n        consoleRef.set(console);\n        return console;\n    }\n\n    public Console(\n            final OutputFormat outputFormat,\n            final KsqlTerminal terminal,\n            final RowCaptor rowCaptor\n    ) {\n        this.outputFormat = Objects.requireNonNull(outputFormat, \"outputFormat\");\n        this.terminal = Objects.requireNonNull(terminal, \"terminal\");\n        this.rowCaptor = Objects.requireNonNull(rowCaptor, \"rowCaptor\");\n        this.cliSpecificCommands = Maps.newLinkedHashMap();\n        this.config = new CliConfig(ImmutableMap.of());\n    }\n\n    public PrintWriter writer() {\n        return terminal.writer();\n    }\n\n    public void flush() {\n        terminal.flush();\n    }\n\n    public void setSpool(final File file) {\n        try {\n            terminal.setSpool(new PrintWriter(file, Charset.defaultCharset().name()));\n            spoolFile = Optional.of(file);\n            terminal.writer().println(\"Session will be spooled to \" + file.getAbsolutePath());\n            terminal.writer().println(\"Enter SPOOL OFF to disable\");\n        } catch (final IOException e) {\n            throw new KsqlException(\"Cannot SPOOL to file: \" + file, e);\n        }\n    }\n\n    public void unsetSpool() {\n        terminal.unsetSpool();\n        spoolFile.ifPresent(f -> terminal.writer().println(\"Spool written to \" + f.getAbsolutePath()));\n        spoolFile = Optional.empty();\n    }\n\n    public int getWidth() {\n        return terminal.getWidth();\n    }\n\n    public void clearScreen() {\n        terminal.clearScreen();\n    }\n\n    public StatusClosable setStatusMessage(final String message) {\n        return terminal.setStatusMessage(message);\n    }\n\n    public void handle(final Signal signal, final SignalHandler signalHandler) {\n        terminal.handle(signal, signalHandler);\n    }\n\n    public void setCliProperty(final String name, final Object value) {\n        try {\n            config = config.with(name, value);\n        } catch (final ConfigException e) {\n            terminal.writer().println(e.getMessage());\n        }\n    }\n\n    @Override\n    public void close() {\n        terminal.close();\n    }\n\n    public void addResult(final List<List<String>> rowValues) {\n        rowCaptor.addRows(rowValues);\n    }\n\n    public Map<String, CliSpecificCommand> getCliSpecificCommands() {\n        return new HashMap<>(cliSpecificCommands);\n    }\n\n    public String nextNonCliCommand() {\n        String line;\n\n        do {\n            line = terminal.readLine();\n\n        } while (maybeHandleCliSpecificCommands(line));\n\n        return line;\n    }\n\n    public List<HistoryEntry> getHistory() {\n        return Collections.unmodifiableList(terminal.getHistory());\n    }\n\n    public void printErrorMessage(final KsqlErrorMessage errorMessage) {\n        if (errorMessage instanceof KsqlStatementErrorMessage) {\n            printKsqlEntityList(((KsqlStatementErrorMessage) errorMessage).getEntities());\n        }\n        printError(errorMessage.getMessage(), errorMessage.toString());\n    }\n\n    public void printError(final String shortMsg, final String fullMsg) {\n        log.error(fullMsg);\n        terminal.printError(shortMsg);\n    }\n\n    public void printStreamedRow(final StreamedRow row) {\n        row.getErrorMessage().ifPresent(this::printErrorMessage);\n\n        row.getFinalMessage().ifPresent(finalMsg -> writer().println(finalMsg));\n\n        row.getHeader().ifPresent(this::printRowHeader);\n\n        if (row.getRow().isPresent()) {\n            switch (outputFormat) {\n                case JSON:\n                    printAsJson(row.getRow().get());\n                    break;\n                case TABULAR:\n                    printAsTable(row.getRow().get());\n                    break;\n                default:\n                    throw new RuntimeException(String.format(\n                            \"Unexpected output format: '%s'\",\n                            outputFormat.name()\n                    ));\n            }\n        }\n    }\n\n    public void printKsqlEntityList(final List<KsqlEntity> entityList) {\n        switch (outputFormat) {\n            case JSON:\n                printAsJson(entityList);\n                break;\n            case TABULAR:\n                final boolean showStatements = entityList.size() > 1;\n                for (final KsqlEntity ksqlEntity : entityList) {\n                    writer().println();\n                    if (showStatements) {\n                        writer().println(ksqlEntity.getStatementText());\n                    }\n                    printAsTable(ksqlEntity);\n                }\n                break;\n            default:\n                throw new RuntimeException(String.format(\n                        \"Unexpected output format: '%s'\",\n                        outputFormat.name()\n                ));\n        }\n    }\n\n    private void printRowHeader(final Header header) {\n        switch (outputFormat) {\n            case JSON:\n                printAsJson(header);\n                break;\n            case TABULAR:\n                writer().println(\n                        TabularRow.createHeader(\n                                getWidth(),\n                                header.getSchema().columns(),\n                                config.getString(CliConfig.WRAP_CONFIG).equalsIgnoreCase(OnOff.ON.toString()),\n                                config.getInt(CliConfig.COLUMN_WIDTH_CONFIG)\n                        )\n                );\n                break;\n            default:\n                throw new RuntimeException(String.format(\n                        \"Unexpected output format: '%s'\",\n                        outputFormat.name()\n                ));\n        }\n    }\n\n    public void registerCliSpecificCommand(final CliSpecificCommand cliSpecificCommand) {\n        cliSpecificCommands.put(cliSpecificCommand.getName().toLowerCase(), cliSpecificCommand);\n    }\n\n    public void setOutputFormat(final String newFormat) {\n        try {\n            outputFormat = OutputFormat.get(newFormat);\n            writer().printf(\"Output format set to %s%n\", outputFormat.name());\n        } catch (final IllegalArgumentException exception) {\n            writer().printf(\n                    \"Invalid output format: '%s' (valid formats: %s)%n\",\n                    newFormat,\n                    OutputFormat.VALID_FORMATS\n            );\n        }\n    }\n\n    public OutputFormat getOutputFormat() {\n        return outputFormat;\n    }\n\n    private Optional<CliCmdExecutor> getCliCommand(final String line) {\n        final List<String> parts = splitByUnquotedWhitespace(StringUtils.stripEnd(line.trim(), \";\"));\n        if (parts.isEmpty()) {\n            return Optional.empty();\n        }\n\n        final String command = String.join(\" \", parts);\n\n        return cliSpecificCommands.values().stream()\n                .filter(cliSpecificCommand -> cliSpecificCommand.matches(command))\n                .map(cliSpecificCommand -> CliCmdExecutor.of(cliSpecificCommand, parts))\n                .findFirst();\n    }\n\n    private void printAsTable(final DataRow row) {\n        rowCaptor.addRow(row);\n\n        final boolean tombstone = row.getTombstone().orElse(false);\n\n        final List<?> columns = tombstone\n                ? row.getColumns().stream()\n                .map(val -> val == null ? \"<TOMBSTONE>\" : val)\n                .collect(Collectors.toList())\n                : row.getColumns();\n\n        writer().println(TabularRow.createRow(\n                getWidth(),\n                columns,\n                config.getString(CliConfig.WRAP_CONFIG).equalsIgnoreCase(OnOff.ON.toString()),\n                config.getInt(CliConfig.COLUMN_WIDTH_CONFIG))\n        );\n\n        flush();\n    }\n\n    private void printAsTable(final KsqlEntity entity) {\n        final Handler1<KsqlEntity, Console> handler = PRINT_HANDLERS.get(entity.getClass());\n\n        if (handler == null) {\n            throw new RuntimeException(String.format(\n                    \"Unexpected KsqlEntity class: '%s'\", entity.getClass().getCanonicalName()\n            ));\n        }\n\n        handler.handle(this, entity);\n\n        printWarnings(entity);\n    }\n\n    private void printWarnings(final KsqlEntity entity) {\n        for (final KsqlWarning warning : entity.getWarnings()) {\n            writer().println(\"WARNING: \" + warning.getMessage());\n        }\n    }\n\n    private static String formatFieldType(\n            final FieldInfo field,\n            final Optional<WindowType> windowType,\n            final boolean isTable\n    ) {\n        final FieldType possibleFieldType = field.getType().orElse(null);\n\n        if (possibleFieldType == FieldType.HEADER) {\n            final String headerType = field.getHeaderKey()\n                    .map(k -> \"(header('\" + k + \"'))\")\n                    .orElse(\"(headers)\");\n            return String.format(\"%-16s %s\", field.getSchema().toTypeString(), headerType);\n        }\n\n        if (possibleFieldType == FieldType.KEY) {\n            final String wt = windowType\n                    .map(v -> \" (Window type: \" + v + \")\")\n                    .orElse(\"\");\n\n            final String keyType = isTable ? \"(primary key)\" : \"(key)\";\n            return String.format(\"%-16s %s%s\", field.getSchema().toTypeString(), keyType, wt);\n        }\n\n        return field.getSchema().toTypeString();\n    }\n\n    private void printSchema(\n            final Optional<WindowType> windowType,\n            final List<FieldInfo> fields,\n            final boolean isTable\n    ) {\n        final Table.Builder tableBuilder = new Table.Builder();\n        if (!fields.isEmpty()) {\n            tableBuilder.withColumnHeaders(\"Field\", \"Type\");\n            fields.forEach(f -> tableBuilder.withRow(\n                    f.getName(),\n                    formatFieldType(f, windowType, isTable)\n            ));\n            tableBuilder.build().print(this);\n        }\n    }\n\n    private void printTopicInfo(final SourceDescription source) {\n        final String timestamp = source.getTimestamp().isEmpty()\n                ? \"Not set - using <ROWTIME>\"\n                : source.getTimestamp();\n\n        writer().println(String.format(\"%-20s : %s\", \"Timestamp field\", timestamp));\n        writer().println(String.format(\"%-20s : %s\", \"Key format\", source.getKeyFormat()));\n        writer().println(String.format(\"%-20s : %s\", \"Value format\", source.getValueFormat()));\n\n        if (!source.getTopic().isEmpty()) {\n            String topicInformation = String.format(\"%-20s : %s\",\n                    \"Kafka topic\",\n                    source.getTopic()\n            );\n\n            // If Describe ACLs permissions aren't given for a topic, partitions and replica default to 0\n            // Details aren't printed out if the Describe fails.\n            if (source.getPartitions() != 0) {\n                topicInformation = topicInformation.concat(String.format(\n                        \" (partitions: %d, replication: %d)\",\n                        source.getPartitions(),\n                        source.getReplication()\n                ));\n            }\n            writer().println(topicInformation);\n        }\n    }\n\n    private void printSourceConstraints(final List<String> sourceConstraints) {\n        if (!sourceConstraints.isEmpty()) {\n            writer().println(String.format(\n                    \"%n%-20s%n%-20s\",\n                    \"Sources that have a DROP constraint on this source\",\n                    \"--------------------------------------------------\"\n            ));\n\n            sourceConstraints.forEach(sourceName -> writer().println(sourceName));\n        }\n    }\n\n    private void printQueries(\n            final List<RunningQuery> queries,\n            final String type,\n            final String operation\n    ) {\n        if (!queries.isEmpty()) {\n            writer().println(String.format(\n                    \"%n%-20s%n%-20s\",\n                    \"Queries that \" + operation + \" from this \" + type,\n                    \"-----------------------------------\"\n            ));\n            for (final RunningQuery writeQuery : queries) {\n                writer().println(writeQuery.getId()\n                        + \" (\" + writeQuery.getState().orElse(\"N/A\")\n                        + \") : \" + writeQuery.getQuerySingleLine());\n            }\n            writer().println(\"\\nFor query topology and execution plan please run: EXPLAIN <QueryId>\");\n        }\n    }\n\n    private void printExecutionPlan(final QueryDescription queryDescription) {\n        if (!queryDescription.getExecutionPlan().isEmpty()) {\n            writer().println(String.format(\n                    \"%n%-20s%n%-20s%n%s\",\n                    \"Execution plan\",\n                    \"--------------\",\n                    queryDescription.getExecutionPlan()\n            ));\n        }\n    }\n\n    private void printTopology(final QueryDescription queryDescription) {\n        if (!queryDescription.getTopology().isEmpty()) {\n            writer().println(String.format(\n                    \"%n%-20s%n%-20s%n%s\",\n                    \"Processing topology\",\n                    \"-------------------\",\n                    queryDescription.getTopology()\n            ));\n        }\n    }\n\n    private void printOverriddenProperties(final QueryDescription queryDescription) {\n        final Map<String, Object> overriddenProperties = queryDescription.getOverriddenProperties();\n        if (overriddenProperties.isEmpty()) {\n            return;\n        }\n\n        final List<List<String>> rows = overriddenProperties.entrySet().stream()\n                .sorted(Entry.comparingByKey())\n                .map(prop -> Arrays.asList(prop.getKey(), Objects.toString(prop.getValue())))\n                .collect(Collectors.toList());\n\n        new Builder()\n                .withColumnHeaders(\"Property\", \"Value\")\n                .withRows(rows)\n                .withHeaderLine(String.format(\n                        \"%n%-20s%n%-20s\",\n                        \"Overridden Properties\",\n                        \"---------------------\"))\n                .build()\n                .print(this);\n    }\n\n    private void printQueryError(final QueryDescription query) {\n        writer().println();\n\n        final DateTimeFormatter dateFormatter =\n                DateTimeFormatter.ofPattern(\"yyyy-MM-dd hh:mm:ss,SSS (z)\");\n        for (final QueryError error : query.getQueryErrors()) {\n            final Instant ts = Instant.ofEpochMilli(error.getTimestamp());\n            final String errorDate = ts.atZone(ZoneId.systemDefault()).format(dateFormatter);\n\n            writer().println(String.format(\"%-20s : %s\", \"Error Date\", errorDate));\n            writer().println(String.format(\"%-20s : %s\", \"Error Details\", error.getErrorMessage()));\n            writer().println(String.format(\"%-20s : %s\", \"Error Type\", error.getType()));\n        }\n    }\n\n    private void printStatistics(final SourceDescription source) {\n        final List<QueryHostStat> statistics = source.getClusterStatistics();\n        final List<QueryHostStat> errors = source.getClusterErrorStats();\n\n        if (statistics.isEmpty() && errors.isEmpty()) {\n            writer().println(String.format(\n                    \"%n%-20s%n%s\",\n                    \"Local runtime statistics\",\n                    \"------------------------\"\n            ));\n            writer().println(source.getStatistics());\n            writer().println(source.getErrorStats());\n            return;\n        }\n        final List<String> headers = ImmutableList.of(\"Host\", \"Metric\", \"Value\", \"Last Message\");\n        final Stream<QueryHostStat> rows = Streams.concat(statistics.stream(), errors.stream());\n\n        writer().println(String.format(\n                \"%n%-20s%n%s\",\n                \"Runtime statistics by host\",\n                \"-------------------------\"\n        ));\n        final Table statsTable = new Table.Builder()\n                .withColumnHeaders(headers)\n                .withRows(rows\n                        .sorted(Comparator\n                                .comparing(QueryHostStat::host)\n                                .thenComparing(Stat::name)\n                        )\n                        .map((metric) -> {\n                            final String hostCell = metric.host().toString();\n                            final String formattedValue = String.format(\"%10.0f\", metric.getValue());\n                            return ImmutableList.of(hostCell, metric.name(), formattedValue, metric.timestamp());\n                        }))\n                .build();\n        statsTable.print(this);\n    }\n\n    private void printSourceDescription(final SourceDescription source) {\n        final boolean isTable = source.getType().equalsIgnoreCase(\"TABLE\");\n\n        writer().println(String.format(\"%-20s : %s\", \"Name\", source.getName()));\n        if (!source.isExtended()) {\n            printSchema(source.getWindowType(), source.getFields(), isTable);\n            writer().println(\n                    \"For runtime statistics and query details run: DESCRIBE <Stream,Table> EXTENDED;\");\n            return;\n        }\n        writer().println(String.format(\"%-20s : %s\", \"Type\", source.getType()));\n\n        printTopicInfo(source);\n        writer().println(String.format(\"%-20s : %s\", \"Statement\", source.getStatement()));\n        writer().println(\"\");\n\n        printSchema(source.getWindowType(), source.getFields(), isTable);\n\n        printSourceConstraints(source.getSourceConstraints());\n\n        printQueries(source.getReadQueries(), source.getType(), \"read\");\n\n        printQueries(source.getWriteQueries(), source.getType(), \"write\");\n        printStatistics(source);\n\n        writer().println(String.format(\n                \"(%s)\",\n                \"Statistics of the local KSQL server interaction with the Kafka topic \"\n                        + source.getTopic()\n        ));\n        if (!source.getQueryOffsetSummaries().isEmpty()) {\n            writer().println();\n            writer().println(\"Consumer Groups summary:\");\n            for (QueryOffsetSummary entry : source.getQueryOffsetSummaries()) {\n                writer().println();\n                writer().println(String.format(\"%-20s : %s\", \"Consumer Group\", entry.getGroupId()));\n                if (entry.getTopicSummaries().isEmpty()) {\n                    writer().println(\"<no offsets committed by this group yet>\");\n                }\n                for (QueryTopicOffsetSummary topicSummary : entry.getTopicSummaries()) {\n                    writer().println();\n                    writer().println(String.format(\"%-20s : %s\",\n                            \"Kafka topic\", topicSummary.getKafkaTopic()));\n                    writer().println(String.format(\"%-20s : %s\",\n                            \"Max lag\", topicSummary.getOffsets().stream()\n                                    .mapToLong(s -> s.getLogEndOffset() - s.getConsumerOffset())\n                                    .max()\n                                    .orElse(0)));\n                    writer().println(\"\");\n                    final Table taskTable = new Table.Builder()\n                            .withColumnHeaders(\n                                    ImmutableList.of(\"Partition\", \"Start Offset\", \"End Offset\", \"Offset\", \"Lag\"))\n                            .withRows(topicSummary.getOffsets()\n                                    .stream()\n                                    .map(offset -> ImmutableList.of(\n                                            String.valueOf(offset.getPartition()),\n                                            String.valueOf(offset.getLogStartOffset()),\n                                            String.valueOf(offset.getLogEndOffset()),\n                                            String.valueOf(offset.getConsumerOffset()),\n                                            String.valueOf(offset.getLogEndOffset() - offset.getConsumerOffset())\n                                    )))\n                            .build();\n                    taskTable.print(this);\n                }\n            }\n        }\n    }\n\n    private void printSourceDescriptionList(final SourceDescriptionList sourceDescriptionList) {\n        sourceDescriptionList.getSourceDescriptions().forEach(\n                sourceDescription -> {\n                    printSourceDescription(sourceDescription);\n                    writer().println();\n                });\n    }\n\n    private void printQuerySources(final QueryDescription query) {\n        if (!query.getSources().isEmpty()) {\n            writer().println(String.format(\n                    \"%n%-20s%n%-20s\",\n                    \"Sources that this query reads from: \",\n                    \"-----------------------------------\"\n            ));\n            for (final String sources : query.getSources()) {\n                writer().println(sources);\n            }\n            writer().println(\"\\nFor source description please run: DESCRIBE [EXTENDED] <SourceId>\");\n        }\n    }\n\n    private void printQuerySinks(final QueryDescription query) {\n        if (!query.getSinks().isEmpty()) {\n            writer().println(String.format(\n                    \"%n%-20s%n%-20s\",\n                    \"Sinks that this query writes to: \",\n                    \"-----------------------------------\"\n            ));\n            for (final String sinks : query.getSinks()) {\n                writer().println(sinks);\n            }\n            writer().println(\"\\nFor sink description please run: DESCRIBE [EXTENDED] <SinkId>\");\n        }\n    }\n\n    private void printQueryDescription(final QueryDescription query) {\n        writer().println(String.format(\"%-20s : %s\", \"ID\", query.getId()));\n        writer().println(String.format(\"%-20s : %s\", \"Query Type\", query.getQueryType()));\n        if (query.getStatementText().length() > 0) {\n            writer().println(String.format(\"%-20s : %s\", \"SQL\", query.getStatementText()));\n        }\n        if (!query.getKsqlHostQueryStatus().isEmpty()) {\n            writer().println(String.format(\n                    \"%-20s : %s\", \"Host Query Status\",\n                    query.getKsqlHostQueryStatus()));\n        }\n        writer().println();\n        printSchema(query.getWindowType(), query.getFields(), false);\n        printQuerySources(query);\n        printQuerySinks(query);\n        printExecutionPlan(query);\n        printTopology(query);\n        printOverriddenProperties(query);\n        printQueryError(query);\n    }\n\n    private void printConnectorDescription(final ConnectorDescription description) {\n        final ConnectorStateInfo status = description.getStatus();\n        writer().println(String.format(\"%-20s : %s\", \"Name\", status.name()));\n        writer().println(String.format(\"%-20s : %s\", \"Class\", description.getConnectorClass()));\n        writer().println(String.format(\"%-20s : %s\", \"Type\", description.getStatus().type()));\n        writer().println(String.format(\"%-20s : %s\", \"State\", status.connector().state()));\n        writer().println(String.format(\"%-20s : %s\", \"WorkerId\", status.connector().workerId()));\n        if (!ObjectUtils.defaultIfNull(status.connector().trace(), \"\").isEmpty()) {\n            writer().println(String.format(\"%-20s : %s\", \"Trace\", status.connector().trace()));\n        }\n\n        if (!status.tasks().isEmpty()) {\n            writer().println();\n            final Table taskTable = new Table.Builder()\n                    .withColumnHeaders(ImmutableList.of(\"Task ID\", \"State\", \"Error Trace\"))\n                    .withRows(status.tasks()\n                            .stream()\n                            .map(task -> ImmutableList.of(\n                                    String.valueOf(task.id()),\n                                    task.state(),\n                                    ObjectUtils.defaultIfNull(task.trace(), \"\"))))\n                    .build();\n            taskTable.print(this);\n        }\n\n        if (!description.getSources().isEmpty()) {\n            writer().println();\n            final Table sourceTable = new Table.Builder()\n                    .withColumnHeaders(\"KSQL Source Name\", \"Kafka Topic\", \"Type\")\n                    .withRows(description.getSources()\n                            .stream()\n                            .map(source -> ImmutableList\n                                    .of(source.getName(), source.getTopic(), source.getType())))\n                    .build();\n            sourceTable.print(this);\n        }\n\n        if (!description.getTopics().isEmpty()) {\n            writer().println();\n            final Table topicTable = new Table.Builder()\n                    .withColumnHeaders(\"Related Topics\")\n                    .withRows(description.getTopics().stream().map(ImmutableList::of))\n                    .build();\n            topicTable.print(this);\n        }\n    }\n\n    private void printQueryDescriptionList(final QueryDescriptionList queryDescriptionList) {\n        queryDescriptionList.getQueryDescriptions().forEach(\n                queryDescription -> {\n                    printQueryDescription(queryDescription);\n                    writer().println();\n                });\n    }\n\n    private void printFunctionDescription(final FunctionDescriptionList describeFunction) {\n        final String functionName = describeFunction.getName().toUpperCase();\n        final String baseFormat = \"%-12s: %s%n\";\n        final String subFormat = \"\\t%-12s: %s%n\";\n        writer().printf(baseFormat, \"Name\", functionName);\n        if (!describeFunction.getAuthor().trim().isEmpty()) {\n            writer().printf(baseFormat, \"Author\", describeFunction.getAuthor());\n        }\n        if (!describeFunction.getVersion().trim().isEmpty()) {\n            writer().printf(baseFormat, \"Version\", describeFunction.getVersion());\n        }\n\n        printDescription(baseFormat, \"Overview\", describeFunction.getDescription());\n\n        writer().printf(baseFormat, \"Type\", describeFunction.getType().name());\n        writer().printf(baseFormat, \"Jar\", describeFunction.getPath());\n        writer().printf(baseFormat, \"Variations\", \"\");\n        final Collection<FunctionInfo> functions = describeFunction.getFunctions();\n        functions.forEach(functionInfo -> {\n                    final String arguments = functionInfo.getArguments().stream()\n                            .map(Console::argToString)\n                            .collect(Collectors.joining(\", \"));\n\n                    writer().printf(\"%n\\t%-12s: %s(%s)%n\", \"Variation\", functionName, arguments);\n\n                    writer().printf(subFormat, \"Returns\", functionInfo.getReturnType());\n                    printDescription(subFormat, \"Description\", functionInfo.getDescription());\n                    functionInfo.getArguments()\n                            .forEach(a -> printDescription(subFormat, a.getName(), a.getDescription()));\n                }\n        );\n    }\n\n    private void printAssertTopic(final AssertTopicEntity assertTopic) {\n        final String existence = assertTopic.getExists() ? \" exists\" : \" does not exist\";\n        writer().printf(\"Topic \" + assertTopic.getTopicName() + existence + \".\\n\");\n    }\n\n    private void printAssertSchema(final AssertSchemaEntity assertSchema) {\n        if (!assertSchema.getId().isPresent() && !assertSchema.getSubject().isPresent()) {\n            throw new RuntimeException(\"No subject or id found in AssertSchema response.\");\n        }\n\n        final String existence = assertSchema.getExists() ? \" exists\" : \" does not exist\";\n        final String subject = assertSchema.getSubject().isPresent()\n                ? \" subject \" + assertSchema.getSubject().get()\n                : \"\";\n        final String id = assertSchema.getId().isPresent()\n                ? \" id \" + assertSchema.getId().get()\n                : \"\";\n        writer().printf(\"Schema with\" + subject + id + existence + \".\\n\");\n    }\n\n    private static String argToString(final ArgumentInfo arg) {\n        final String type = arg.getType() + (arg.getIsVariadic() ? \"[]\" : \"\");\n        return arg.getName().isEmpty() ? type : (arg.getName() + \" \" + type);\n    }\n\n    private void printDescription(final String format, final String name, final String description) {\n        final String trimmed = description.trim();\n        if (trimmed.isEmpty()) {\n            return;\n        }\n\n        final int labelLen = String.format(format.replace(\"%n\", \"\"), name, \"\")\n                .replace(\"\\t\", \"  \")\n                .length();\n\n        final int width = Math.max(getWidth(), 80) - labelLen;\n\n        final String fixedWidth = splitLongLine(trimmed, width);\n\n        final String indent = String.format(\"%-\" + labelLen + \"s\", \"\");\n\n        final String result = fixedWidth\n                .replace(System.lineSeparator(), System.lineSeparator() + indent);\n\n        writer().printf(format, name, result);\n    }\n\n    private static String splitLongLine(final String input, final int maxLineLength) {\n        final StringTokenizer spaceTok = new StringTokenizer(input, \" \\n\", true);\n        final StringBuilder output = new StringBuilder(input.length());\n        int lineLen = 0;\n        while (spaceTok.hasMoreTokens()) {\n            final String word = spaceTok.nextToken();\n            final boolean isNewLineChar = word.equals(\"\\n\");\n\n            if (isNewLineChar || lineLen + word.length() > maxLineLength) {\n                output.append(System.lineSeparator());\n                lineLen = 0;\n\n                if (isNewLineChar) {\n                    continue;\n                }\n            }\n\n            output.append(word);\n            lineLen += word.length();\n        }\n        return output.toString();\n    }\n\n    private void printAsJson(final Object o) {\n        try {\n            OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(writer(), o);\n            writer().println();\n            flush();\n        } catch (final IOException e) {\n            throw new RuntimeException(\"Failed to write to console\", e);\n        }\n    }\n\n    static class NoOpRowCaptor implements RowCaptor {\n\n        @Override\n        public void addRow(final DataRow row) {\n        }\n\n        @Override\n        public void addRows(final List<List<String>> fields) {\n        }\n    }\n\n    public boolean maybeHandleCliSpecificCommands(final String line) {\n        if (line == null) {\n            return false;\n        }\n\n        return getCliCommand(line)\n                .map(cmd -> {\n                    cmd.execute(writer());\n                    flush();\n                    return true;\n                })\n                .orElse(false);\n    }\n\n    private static final class CliCmdExecutor {\n\n        private final CliSpecificCommand cmd;\n        private final List<String> args;\n\n\n        private static CliCmdExecutor of(final CliSpecificCommand cmd, final List<String> lineParts) {\n            final String[] nameParts = cmd.getName().split(\"\\\\s+\");\n            final List<String> argList = lineParts.subList(nameParts.length, lineParts.size()).stream()\n                    .map(CmdLineUtil::removeMatchedSingleQuotes)\n                    .collect(Collectors.toList());\n\n            return new CliCmdExecutor(cmd, argList);\n        }\n\n        private CliCmdExecutor(final CliSpecificCommand cmd, final List<String> args) {\n            this.cmd = Objects.requireNonNull(cmd, \"cmd\");\n            this.args = ImmutableList.copyOf(Objects.requireNonNull(args, \"args\"));\n        }\n\n        public void execute(final PrintWriter terminal) {\n            cmd.execute(args, terminal);\n        }\n    }\n}\n"
  }
]