├── .editorconfig ├── .github ├── renovate.json └── workflows │ ├── codeql.yml │ ├── main.yml │ ├── pr-build.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── DownloadJavadocsPlugin.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── icon.png ├── package-lock.json ├── package.json ├── readme.md ├── settings.gradle.kts └── src ├── main ├── java │ └── deser │ │ ├── SerializationDumper.java │ │ └── support │ │ ├── ClassDataDesc.java │ │ ├── ClassDetails.java │ │ └── ClassField.java ├── kotlin │ └── io │ │ └── github │ │ └── paulgriffith │ │ └── kindling │ │ ├── MainPanel.kt │ │ ├── cache │ │ ├── AliasingObjectInputStream.kt │ │ ├── CacheEntry.kt │ │ ├── CacheModel.kt │ │ ├── CacheView.kt │ │ ├── SchemaFilterList.kt │ │ ├── SchemaModel.kt │ │ └── model │ │ │ ├── AlarmJournalData.kt │ │ │ ├── AuditProfileData.kt │ │ │ └── ScriptedSFData.kt │ │ ├── core │ │ ├── CustomIconView.kt │ │ ├── Detail.kt │ │ ├── DetailsPane.kt │ │ ├── Kindling.kt │ │ ├── Tool.kt │ │ └── ToolPanel.kt │ │ ├── idb │ │ ├── IdbView.kt │ │ ├── ImagesTab.kt │ │ ├── generic │ │ │ ├── Column.kt │ │ │ ├── DBMetaDataTree.kt │ │ │ ├── GenericView.kt │ │ │ ├── QueryResult.kt │ │ │ ├── ResultsPanel.kt │ │ │ └── Table.kt │ │ └── metrics │ │ │ ├── Metric.kt │ │ │ ├── MetricCard.kt │ │ │ ├── MetricTree.kt │ │ │ ├── MetricsView.kt │ │ │ └── Sparkline.kt │ │ ├── internal │ │ ├── DetailsIcon.kt │ │ ├── DetailsModel.kt │ │ └── FileTransferHandler.kt │ │ ├── log │ │ ├── Header.kt │ │ ├── LogPanel.kt │ │ ├── LoggerNames.kt │ │ ├── SystemLogsEvent.kt │ │ ├── WrapperLogView.kt │ │ └── models.kt │ │ ├── thread │ │ ├── FilterList.kt │ │ ├── MultiThreadView.kt │ │ ├── ThreadComparisonPane.kt │ │ ├── ThreadDumpCheckboxList.kt │ │ └── model │ │ │ ├── NoneAsNullStringSerializer.kt │ │ │ ├── Thread.kt │ │ │ ├── ThreadDump.kt │ │ │ └── ThreadModel.kt │ │ ├── utils │ │ ├── Action.kt │ │ ├── Column.kt │ │ ├── ColumnList.kt │ │ ├── ScrollingTextPane.kt │ │ ├── TabStrip.kt │ │ ├── ZipFileTree.kt │ │ ├── swing.kt │ │ └── utils.kt │ │ └── zip │ │ ├── ZipView.kt │ │ └── views │ │ ├── GenericFileView.kt │ │ ├── ImageView.kt │ │ ├── MultiToolView.kt │ │ ├── PathView.kt │ │ ├── ProjectView.kt │ │ ├── TextFileView.kt │ │ └── ToolView.kt └── resources │ ├── icons │ ├── bx-archive.svg │ ├── bx-box.svg │ ├── bx-chip.svg │ ├── bx-clipboard.svg │ ├── bx-cog.svg │ ├── bx-column.svg │ ├── bx-data.svg │ ├── bx-detail.svg │ ├── bx-error.svg │ ├── bx-file-find.svg │ ├── bx-file.svg │ ├── bx-hdd.svg │ ├── bx-image.svg │ ├── bx-link-external.svg │ ├── bx-moon.svg │ ├── bx-save.svg │ ├── bx-search.svg │ ├── bx-sort-a-z.svg │ ├── bx-sort-down.svg │ ├── bx-sort-up.svg │ ├── bx-sort-z-a.svg │ ├── bx-sun.svg │ ├── bx-table.svg │ ├── ignition.icns │ ├── ignition.ico │ ├── ignition.png │ ├── kindling.png │ └── null.svg │ └── logback.xml └── test ├── kotlin └── io │ └── github │ └── paulgriffith │ └── kindling │ ├── log │ └── WrapperLogParsingTests.kt │ └── thread │ └── ThreadViewTests.kt └── resources └── io └── github └── paulgriffith └── kindling └── thread ├── deadlockThreadDump.json ├── legacyDeadlockThreadDump.txt ├── legacyScriptThreadDump.txt ├── legacyWebThreadDump.txt └── threadDump.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | ij_kotlin_allow_trailing_comma=true 3 | ij_kotlin_allow_trailing_comma_on_call_site=true 4 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "ignoreDeps": [ 7 | "com.inductiveautomation.ignition:gateway-api", 8 | "com.inductiveautomation.ignition:common", 9 | "org.apache.commons:commons-lang3", 10 | "com.google.guava:guava" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '23 5 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'java' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish snapshot package on commit to main 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | jobs: 7 | build: 8 | runs-on: "ubuntu-latest" 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-java@v3 12 | with: 13 | distribution: 'zulu' 14 | java-version: 17 15 | cache: 'gradle' 16 | - name: Execute Gradle build 17 | run: ./gradlew build 18 | - name: Echo project version 19 | run: echo "{project_version}=`./gradlew printVersion --quiet`" >> $GITHUB_ENV 20 | - name: Delete existing snapshot, if any 21 | uses: actions/delete-package-versions@v4 22 | with: 23 | package-version-ids: ${{ env.project_version }} 24 | package-name: 'io.github.paulgriffith.kindling' 25 | package-type: 'maven' 26 | continue-on-error: true 27 | - name: Publish to Github Packages 28 | run: ./gradlew publish 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/pr-build.yml: -------------------------------------------------------------------------------- 1 | name: Build PRs 2 | on: pull_request 3 | jobs: 4 | gradle: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: actions/setup-java@v3 9 | with: 10 | distribution: 'zulu' 11 | java-version: 17 12 | cache: 'gradle' 13 | - name: Execute Gradle build 14 | run: ./gradlew build 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release new version upon tag commit 2 | on: 3 | push: 4 | tags: 5 | - '[0-9].[0-9].[0-9]' 6 | jobs: 7 | build: 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest] 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-java@v3 15 | with: 16 | distribution: 'zulu' 17 | java-version: 17 18 | cache: 'gradle' 19 | - name: Execute Gradle build, JPackage 20 | run: './gradlew -Pversion="${{github.ref_name}}" build jpackage' 21 | - name: Upload artifacts 22 | uses: actions/upload-artifact@v3 23 | with: 24 | name: ${{ matrix.os }} 25 | path: | 26 | build/jpackage/*.deb 27 | build/jpackage/*.rpm 28 | build/jpackage/*.exe 29 | build/jpackage/*.msi 30 | - name: Upload jar 31 | if: ${{ matrix.os == 'ubuntu-latest' }} 32 | uses: actions/upload-artifact@v3 33 | with: 34 | name: fatjar 35 | path: build/libs/kindling-bundle.jar 36 | - name: Publish to Github packages 37 | if: ${{ matrix.os == 'ubuntu-latest' }} 38 | run: './gradlew -Pversion="${{github.ref_name}}" publish' 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | release: 42 | runs-on: ubuntu-latest 43 | needs: [build] 44 | steps: 45 | - uses: actions/checkout@v3 46 | - name: Download Linux Artifacts 47 | uses: actions/download-artifact@v3 48 | with: 49 | name: ubuntu-latest 50 | path: artifacts 51 | - name: Download Windows Artifacts 52 | uses: actions/download-artifact@v3 53 | with: 54 | name: windows-latest 55 | path: artifacts 56 | - name: Download bundle jar 57 | uses: actions/download-artifact@v3 58 | with: 59 | name: fatjar 60 | - name: Display structure of downloaded files 61 | run: ls -R 62 | - name: Create Release 63 | uses: marvinpinto/action-automatic-releases@latest 64 | with: 65 | repo_token: ${{ secrets.GITHUB_TOKEN }} 66 | prerelease: false 67 | files: | 68 | artifacts/* 69 | kindling-bundle.jar 70 | - name: Set package version 71 | run: npm --no-git-tag-version version ${{github.ref_name}} 72 | - name: Build App Installer Bundles 73 | uses: shannah/jdeploy@4.0.20 74 | with: 75 | github_token: ${{ github.token }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .gradle/ 3 | .idea/ 4 | 5 | **build/ 6 | jdeploy/ 7 | jdeploy-bundle/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Paul Griffith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.internal.os.OperatingSystem 2 | import org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE 3 | import java.time.LocalDate 4 | 5 | plugins { 6 | alias(libs.plugins.kotlin) 7 | alias(libs.plugins.serialization) 8 | alias(libs.plugins.ktlint) 9 | application 10 | alias(libs.plugins.shadow) 11 | alias(libs.plugins.runtime) 12 | `maven-publish` 13 | } 14 | 15 | apply { 16 | plugin() 17 | } 18 | 19 | repositories { 20 | mavenCentral() 21 | maven { 22 | url = uri("https://nexus.inductiveautomation.com/repository/inductiveautomation-releases/") 23 | } 24 | maven { 25 | url = uri("https://nexus.inductiveautomation.com/repository/inductiveautomation-thirdparty/") 26 | } 27 | maven { 28 | url = uri("https://jitpack.io") 29 | content { 30 | includeGroup("com.github.Dansoftowner") 31 | } 32 | } 33 | } 34 | 35 | dependencies { 36 | // see gradle/libs.version.toml 37 | api(libs.serialization.json) 38 | api(libs.xerial.jdbc) 39 | api(libs.hsql) 40 | api(libs.zip4j) 41 | api(libs.miglayout) 42 | api(libs.jide.common) 43 | api(libs.swingx) 44 | api(libs.logback) 45 | api(libs.svgSalamander) 46 | api(libs.bundles.coroutines) 47 | api(libs.bundles.flatlaf) 48 | api(libs.bundles.ignition) { 49 | // Exclude transitive IA dependencies - we only need core Ignition classes for cache deserialization 50 | isTransitive = false 51 | } 52 | api(libs.excelkt) 53 | api(libs.jfreechart) 54 | api(libs.rsyntaxtextarea) 55 | implementation(libs.osthemedetector) 56 | runtimeOnly(libs.bundles.ia.transitive) 57 | 58 | testImplementation(libs.bundles.kotest) 59 | } 60 | 61 | group = "io.github.paulgriffith" 62 | 63 | application { 64 | mainClass.set("io.github.paulgriffith.kindling.MainPanel") 65 | } 66 | 67 | tasks { 68 | test { 69 | useJUnitPlatform() 70 | } 71 | 72 | val cleanupJDeploy by registering(Delete::class) { 73 | delete("jdeploy", "jdeploy-bundle") 74 | } 75 | clean { 76 | finalizedBy(cleanupJDeploy) 77 | } 78 | 79 | shadowJar { 80 | manifest { 81 | attributes["Main-Class"] = "io.github.paulgriffith.kindling.MainPanel" 82 | } 83 | archiveBaseName.set("kindling-bundle") 84 | archiveClassifier.set("") 85 | archiveVersion.set("") 86 | mergeServiceFiles() 87 | } 88 | 89 | register("printVersion") { 90 | doLast { // add a task action 91 | println(project.version) 92 | } 93 | } 94 | } 95 | 96 | kotlin { 97 | jvmToolchain(libs.versions.java.map(String::toInt).get()) 98 | } 99 | 100 | ktlint { 101 | reporters { 102 | reporter(CHECKSTYLE) 103 | } 104 | } 105 | 106 | runtime { 107 | options.set(listOf("--strip-debug", "--compress", "2", "--no-header-files", "--no-man-pages")) 108 | 109 | modules.set( 110 | listOf( 111 | "java.desktop", 112 | "java.sql", 113 | "java.logging", 114 | "java.naming", 115 | "java.xml", 116 | "jdk.zipfs", 117 | ), 118 | ) 119 | 120 | jpackage { 121 | val currentOs = OperatingSystem.current() 122 | val imgType = if (currentOs.isWindows) "ico" else "png" 123 | appVersion = project.version.toString() 124 | imageOptions = listOf("--icon", "src/main/resources/icons/ignition.$imgType") 125 | val options: Map = buildMap { 126 | put("resource-dir", "src/main/resources") 127 | put("vendor", "Paul Griffith") 128 | put("copyright", LocalDate.now().year.toString()) 129 | put("description", "A collection of useful tools for troubleshooting Ignition") 130 | 131 | when { 132 | currentOs.isWindows -> { 133 | put("win-per-user-install", null) 134 | put("win-dir-chooser", null) 135 | put("win-menu", null) 136 | put("win-shortcut", null) 137 | // random (consistent) UUID makes upgrades smoother 138 | put("win-upgrade-uuid", "8e7428c8-bbc6-460a-9995-db6d8b04a690") 139 | } 140 | 141 | currentOs.isLinux -> { 142 | put("linux-shortcut", null) 143 | } 144 | } 145 | } 146 | 147 | // add-exports is used to bypass Java modular restrictions 148 | jvmArgs = listOf("--add-exports", "java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED") 149 | 150 | installerOptions = options.flatMap { (key, value) -> 151 | listOfNotNull("--$key", value) 152 | } 153 | 154 | imageName = "kindling" 155 | installerName = "kindling" 156 | mainJar = "kindling-bundle.jar" 157 | } 158 | } 159 | 160 | configure { 161 | repositories { 162 | maven { 163 | name = "GitHubPackages" 164 | url = uri("https://maven.pkg.github.com/paul-griffith/kindling") 165 | credentials { 166 | username = System.getenv("GITHUB_ACTOR") 167 | password = System.getenv("GITHUB_TOKEN") 168 | } 169 | } 170 | } 171 | publications { 172 | register("gpr") { 173 | from(components["kotlin"]) 174 | pom { 175 | description.set("Kindling core API and first-party tools, packaged for ease of extension by third parties.") 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | implementation(libs.jsoup) 11 | } 12 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kindling-build" 2 | 3 | dependencyResolutionManagement { 4 | versionCatalogs { 5 | create("libs") { 6 | from(files("../gradle/libs.versions.toml")) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/DownloadJavadocsPlugin.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | import org.gradle.api.Task 4 | import org.gradle.api.tasks.SourceSetContainer 5 | import org.gradle.kotlin.dsl.get 6 | import org.gradle.kotlin.dsl.getByName 7 | import org.jsoup.Jsoup 8 | import java.net.URI 9 | import java.net.URL 10 | 11 | data class JavadocUrl( 12 | val base: String, 13 | val noframe: Boolean = false, 14 | ) { 15 | val url: URL 16 | get() = URI("${base}allclasses${if (noframe) "-noframe" else ""}.html").toURL() 17 | } 18 | 19 | private fun javadoc(url: String) = JavadocUrl(url) 20 | private fun legacyJavadoc(url: String) = JavadocUrl(url, noframe = true) 21 | 22 | class DownloadJavadocsPlugin : Plugin { 23 | private val toDownload = mapOf( 24 | "8.1" to listOf( 25 | javadoc("https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.21/"), 26 | javadoc("https://docs.oracle.com/en/java/javase/11/docs/api/"), 27 | legacyJavadoc("https://www.javadoc.io/static/org.python/jython-standalone/2.7.1/") 28 | ), 29 | "8.0" to listOf( 30 | javadoc("https://files.inductiveautomation.com/sdk/javadoc/ignition80/8.0.14/"), 31 | javadoc("https://docs.oracle.com/en/java/javase/11/docs/api/"), 32 | legacyJavadoc("https://www.javadoc.io/static/org.python/jython-standalone/2.7.1/") 33 | ), 34 | "7.9" to listOf( 35 | legacyJavadoc("https://files.inductiveautomation.com/sdk/javadoc/ignition79/7921/"), 36 | legacyJavadoc("https://docs.oracle.com/javase/8/docs/api/"), 37 | legacyJavadoc("https://www.javadoc.io/static/org.python/jython-standalone/2.5.3/") 38 | ), 39 | ) 40 | 41 | override fun apply(target: Project) { 42 | val downloadJavadocs = target.tasks.register("downloadJavadocs", Task::class.java) { 43 | val javadocsDir = temporaryDir.resolve("javadocs") 44 | 45 | for ((version, urls) in toDownload) { 46 | javadocsDir.resolve(version).apply { 47 | mkdirs() 48 | resolve("links.properties").printWriter().use { writer -> 49 | for (javadocUrl in urls) { 50 | javadocUrl.url.openStream().use { inputstream -> 51 | Jsoup.parse(inputstream, Charsets.UTF_8.name(), "") 52 | .select("a[href]") 53 | .forEach { a -> 54 | val className = a.text() 55 | val packageName = a.attr("title").substringAfterLast(' ') 56 | 57 | writer.append(packageName).append('.').append(className) 58 | .append('=').append(javadocUrl.base).append(a.attr("href")) 59 | .appendLine() 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | javadocsDir.resolve("versions.txt").printWriter().use { writer -> 68 | for (version in toDownload.keys) { 69 | writer.println(version) 70 | } 71 | } 72 | 73 | outputs.dir(temporaryDir) 74 | } 75 | 76 | target.extensions.getByName("sourceSets")["main"].resources.srcDir(downloadJavadocs) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=0.6.2-SNAPSHOT 2 | org.gradle.jvmargs=-Xmx2G 3 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | java = "17" 3 | kotlin = "1.8.21" 4 | coroutines = "1.6.4" 5 | flatlaf = "3.1.1" 6 | kotest = "5.6.1" 7 | ignition = "8.1.1" 8 | 9 | [plugins] 10 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 11 | serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 12 | ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "11.3.2" } 13 | shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } 14 | runtime = { id = "org.beryx.runtime", version = "1.13.0" } 15 | 16 | [libraries] 17 | # core functionality 18 | coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } 19 | coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "coroutines" } 20 | serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.5.0" } 21 | xerial-jdbc = { group = "org.xerial", name = "sqlite-jdbc", version = "3.41.2.1" } 22 | logback = { group = "ch.qos.logback", name = "logback-classic", version = "1.4.7" } 23 | hsql = { group = "org.hsqldb", name = "hsqldb", version = "2.7.1" } 24 | zip4j = { group = "net.lingala.zip4j", name = "zip4j", version = "2.11.4" } 25 | excelkt = { group = "io.github.evanrupert", name = "excelkt", version = "1.0.2" } 26 | 27 | # build 28 | jsoup = { group = "org.jsoup", name = "jsoup", version = "1.15.4" } 29 | 30 | # appearance/swing 31 | miglayout = { group = "com.miglayout", name = "miglayout-swing", version = "11.1" } 32 | flatlaf-core = { group = "com.formdev", name = "flatlaf", version.ref = "flatlaf" } 33 | flatlaf-extras = { group = "com.formdev", name = "flatlaf-extras", version.ref = "flatlaf" } 34 | flatlaf-jide = { group = "com.formdev", name = "flatlaf-jide-oss", version.ref = "flatlaf" } 35 | flatlaf-swingx = { group = "com.formdev", name = "flatlaf-swingx", version.ref = "flatlaf" } 36 | svgSalamander = { group = "com.formdev", name = "svgSalamander", version = "1.1.4" } 37 | jide-common = { group = "com.formdev", name = "jide-oss", version = "3.7.12" } 38 | swingx = { group = "org.swinglabs.swingx", name = "swingx-core", version = "1.6.5-1" } 39 | osthemedetector = { group = "com.github.Dansoftowner", name = "jSystemThemeDetector", version = "3.8" } 40 | rsyntaxtextarea = { group = "com.fifesoft", name = "rsyntaxtextarea", version = "3.3.3" } 41 | jfreechart = { group = "org.jfree", name = "jfreechart", version = "1.5.4" } 42 | 43 | # Ignition 44 | ignition-common = { group = "com.inductiveautomation.ignition", name = "common", version.ref = "ignition" } 45 | ignition-gateway = { group = "com.inductiveautomation.ignition", name = "gateway-api", version.ref = "ignition" } 46 | # Ignition core types use classes from these libs (e.g. StringUtils, ImmutableMap), so we're forced to include these 47 | apache-commons = { group = "org.apache.commons", name = "commons-lang3", version = "3.8.1" } 48 | google-guava = { module = "com.google.guava:guava", version = "26.0-jre" } 49 | ia-gson = { module = "com.inductiveautomation.ignition:ia-gson", version = "2.8.5" } 50 | 51 | # test framework 52 | kotest-junit = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotest" } 53 | kotest-assertions-core = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" } 54 | 55 | [bundles] 56 | coroutines = [ 57 | "coroutines-core", 58 | "coroutines-swing", 59 | ] 60 | flatlaf = [ 61 | "flatlaf-core", 62 | "flatlaf-extras", 63 | "flatlaf-jide", 64 | "flatlaf-swingx", 65 | ] 66 | kotest = [ 67 | "kotest-junit", 68 | "kotest-assertions-core", 69 | ] 70 | ignition = [ 71 | "ignition-common", 72 | "ignition-gateway", 73 | ] 74 | ia-transitive = [ 75 | "apache-commons", 76 | "google-guava", 77 | "ia-gson", 78 | ] 79 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paul-griffith/kindling/bf963b2d7bab04414ed418059f94825e039d33ce/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paul-griffith/kindling/bf963b2d7bab04414ed418059f94825e039d33ce/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "bin": { 3 | "kindling": "jdeploy-bundle/jdeploy.js" 4 | }, 5 | "author": "Paul Griffith", 6 | "description": "A standalone collection of utilities to help Ignition users.", 7 | "main": "index.js", 8 | "preferGlobal": true, 9 | "repository": "", 10 | "jdeploy": { 11 | "jdk": false, 12 | "args": [], 13 | "javaVersion": "17", 14 | "documentTypes": [], 15 | "jar": "kindling-bundle.jar", 16 | "javafx": false, 17 | "title": "Kindling" 18 | }, 19 | "dependencies": { 20 | "jdeploy": "^4.0.0", 21 | "njre": "^0.2.0", 22 | "shelljs": "^0.8.4" 23 | }, 24 | "license": "MIT", 25 | "name": "ignition-kindling", 26 | "files": [ 27 | "jdeploy-bundle" 28 | ], 29 | "homepage": "https://github.com/paul-griffith/kindling" 30 | } 31 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Kindling 2 | 3 | A standalone collection of utilities to help [Ignition](https://inductiveautomation.com/) users. Features various tools 4 | to help work with Ignition's custom data export formats. 5 | 6 | ## Tools 7 | 8 | ### Thread Viewer 9 | 10 | Parses Ignition thread dump .json files (generated by all Ignition mechanisms since 8.1.10). 11 | 12 | ### IDB Viewer 13 | 14 | Opens Ignition .idb files and displays a list of tables and allows arbitrary SQL queries to be executed. 15 | 16 | If the file is detected as an Ignition log or metrics file, opens a custom view automatically. Right-click the tab to 17 | switch between IDB views. 18 | 19 | ### Log Viewer 20 | 21 | Open one (or multiple) wrapper.log files. If the output format is Ignition's default, they will be automatically parsed 22 | and presented in the same log view used for system logs. If multiple files are selected, an attempt will be made to 23 | sequence them and present as a single view. 24 | 25 | ### Archive Explorer 26 | 27 | Opens a zip file (including Ignition files like `.gwbk` or `.modl`). Allows opening other tools against the files within 28 | the zip, including the .idb files in a gateway backup. 29 | 30 | ## Store and Forward Cache Viewer 31 | 32 | Opens the [HSQLDB](http://hsqldb.org/) file that contains the Store and Forward disk cache. Attempts to parse the 33 | Java-serialized data within into its object representation. If unable to deserialize (e.g. due to a missing class), 34 | falls back to a string explanation of the serialized data. 35 | 36 | Note: If you encounter any issues with missing classes, please file an issue. 37 | 38 | ## Usage 39 | 40 | Download the hosted installer from [JDeploy](https://www.jdeploy.com/~ignition-kindling) here. These installers allow 41 | for auto-updating upon launch. 42 | If you prefer an offline installation, or to avoid JDeploy, you can also download native installers from 43 | the [Releases page](https://github.com/paul-griffith/kindling/releases). 44 | 45 | ## Development 46 | 47 | Kindling uses Java Swing as a GUI framework, but is written almost exclusively in Kotlin, an alternate JVM language. 48 | Gradle is used as the build tool, and will automatically download the appropriate Gradle and Java version (via the 49 | Gradle wrapper). Most IDEs (Eclipse, IntelliJ) should figure out the project structure automatically. You can directly 50 | run the main class in your IDE ([`MainPanel`](src/main/kotlin/io/github/paulgriffith/kindling/MainPanel.kt)), or you 51 | can run the application via`./gradlew run` at the command line. 52 | 53 | ## Extension 54 | 55 | Kindling uses 56 | the [`ServiceLoader`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ServiceLoader.html) 57 | mechanism to register tools. Simply provide an implementation 58 | of [`io.github.paulgriffith.kindling.core.Tool`](src/main/kotlin/io/github/paulgriffith/kindling/core/Tool.kt) (or 59 | any of its extensions), appropriately registered on the classpath, to add tools at runtime. 60 | 61 | ## Contribution 62 | 63 | Contributions of any kind (additional tools, polish to existing tools, test files) are welcome. 64 | 65 | ## Releases 66 | 67 | New tags pushed to GitHub will automatically trigger an action-based deployment of the given version, which will trigger 68 | JDeploy to fetch the new version upon next launch. Offline installers (created using jpackage) are also available for 69 | Windows and Linux. 70 | 71 | ## Acknowledgements 72 | 73 | - [BoxIcons](https://github.com/atisawd/boxicons) 74 | - [FlatLaf](https://github.com/JFormDesigner/FlatLaf) 75 | - [SerializationDumper](https://github.com/NickstaDB/SerializationDumper) 76 | - [JDeploy](https://www.jdeploy.com/) 77 | 78 | ## Disclaimer 79 | 80 | This is **not** an official Inductive Automation product and is not affiliated with, supported by, maintained by, or 81 | otherwise associated with Inductive Automation in any way. This software is provided as-is with no warranty. 82 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | rootProject.name = "kindling" 10 | 11 | plugins { 12 | id("org.gradle.toolchains.foojay-resolver-convention") version("0.5.0") 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/deser/support/ClassDataDesc.java: -------------------------------------------------------------------------------- 1 | package deser.support; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /*********************************************************** 7 | * Support class for serialization data parsing that holds 8 | * class data details that are required to enable object 9 | * properties and array elements to be read from the 10 | * stream. 11 | *

12 | * Written by Nicky Bloor (@NickstaDB). 13 | **********************************************************/ 14 | public class ClassDataDesc { 15 | /** 16 | * List of all classes making up this class data description (i.e. class, super class, etc) 17 | */ 18 | private final List classDetails; 19 | 20 | /******************* 21 | * Construct the class data description object. 22 | ******************/ 23 | public ClassDataDesc() { 24 | this.classDetails = new ArrayList<>(); 25 | } 26 | 27 | /******************* 28 | * Private constructor which creates a new ClassDataDesc and initialises it 29 | * with a subset of the ClassDetails objects from another. 30 | * 31 | * @param cd The list of ClassDetails objects for the new ClassDataDesc. 32 | ******************/ 33 | private ClassDataDesc(ArrayList cd) { 34 | this.classDetails = cd; 35 | } 36 | 37 | /******************* 38 | * Build a new ClassDataDesc object from the given class index. 39 | *

40 | * This is used to enable classdata to be read from the stream in the case 41 | * where a classDesc element references a super class of another classDesc. 42 | * 43 | * @param index The index to start the new ClassDataDesc from. 44 | * @return A ClassDataDesc describing the classes from the given index. 45 | ******************/ 46 | public ClassDataDesc buildClassDataDescFromIndex(int index) { 47 | ArrayList cd; 48 | 49 | //Build a list of the ClassDetails objects for the new ClassDataDesc 50 | cd = new ArrayList<>(); 51 | for (int i = index; i < this.classDetails.size(); ++i) { 52 | cd.add(this.classDetails.get(i)); 53 | } 54 | 55 | //Return a new ClassDataDesc describing this subset of classes 56 | return new ClassDataDesc(cd); 57 | } 58 | 59 | /******************* 60 | * Add a super class data description to this ClassDataDesc by copying the 61 | * class details across to this one. 62 | * 63 | * @param scdd The ClassDataDesc object describing the super class. 64 | ******************/ 65 | public void addSuperClassDesc(ClassDataDesc scdd) { 66 | //Copy the ClassDetails elements to this ClassDataDesc object 67 | if (scdd != null) { 68 | for (int i = 0; i < scdd.getClassCount(); ++i) { 69 | this.classDetails.add(scdd.getClassDetails(i)); 70 | } 71 | } 72 | } 73 | 74 | /******************* 75 | * Add a class to the ClassDataDesc by name. 76 | * 77 | * @param className The name of the class to add. 78 | ******************/ 79 | public void addClass(String className) { 80 | this.classDetails.add(new ClassDetails(className)); 81 | } 82 | 83 | /******************* 84 | * Set the reference handle of the last class to be added to the 85 | * ClassDataDesc. 86 | * 87 | * @param handle The handle value. 88 | ******************/ 89 | public void setLastClassHandle(int handle) { 90 | this.classDetails.get(this.classDetails.size() - 1).setHandle(handle); 91 | } 92 | 93 | /******************* 94 | * Set the classDescFlags of the last class to be added to the 95 | * ClassDataDesc. 96 | * 97 | * @param classDescFlags The classDescFlags value. 98 | ******************/ 99 | public void setLastClassDescFlags(byte classDescFlags) { 100 | this.classDetails.get(this.classDetails.size() - 1).setClassDescFlags(classDescFlags); 101 | } 102 | 103 | /******************* 104 | * Add a field with the given type code to the last class to be added to 105 | * the ClassDataDesc. 106 | * 107 | * @param typeCode The field type code. 108 | ******************/ 109 | public void addFieldToLastClass(byte typeCode) { 110 | this.classDetails.get(this.classDetails.size() - 1).addField(new ClassField(typeCode)); 111 | } 112 | 113 | /******************* 114 | * Set the name of the last field that was added to the last class to be 115 | * added to the ClassDataDesc. 116 | * 117 | * @param name The field name. 118 | ******************/ 119 | public void setLastFieldName(String name) { 120 | this.classDetails.get(this.classDetails.size() - 1).setLastFieldName(name); 121 | } 122 | 123 | /******************* 124 | * Get the details of a class by index. 125 | * 126 | * @param index The index of the class to retrieve details of. 127 | * @return The requested ClassDetails object. 128 | ******************/ 129 | public ClassDetails getClassDetails(int index) { 130 | return this.classDetails.get(index); 131 | } 132 | 133 | /******************* 134 | * Get the number of classes making up this class data description. 135 | * 136 | * @return The number of classes making up this class data description. 137 | ******************/ 138 | public int getClassCount() { 139 | return this.classDetails.size(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/deser/support/ClassDetails.java: -------------------------------------------------------------------------------- 1 | package deser.support; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static java.io.ObjectStreamConstants.*; 7 | 8 | /*********************************************************** 9 | * Support class for serialization data parsing that holds 10 | * details of a single class to enable class data for that 11 | * class to be read (classDescFlags, field descriptions). 12 | *

13 | * Written by Nicky Bloor (@NickstaDB). 14 | **********************************************************/ 15 | public class ClassDetails { 16 | /** 17 | * The name of the class 18 | */ 19 | private final String className; 20 | /** 21 | * The reference handle for the class 22 | */ 23 | private int refHandle; 24 | /** 25 | * The classDescFlags value for the class 26 | */ 27 | private byte classDescFlags; 28 | /** 29 | * The class field descriptions 30 | */ 31 | private final List fieldDescriptions; 32 | 33 | /******************* 34 | * Construct the ClassDetails object. 35 | * 36 | * @param className The name of the class. 37 | ******************/ 38 | public ClassDetails(String className) { 39 | this.className = className; 40 | this.refHandle = -1; 41 | this.classDescFlags = 0; 42 | this.fieldDescriptions = new ArrayList<>(); 43 | } 44 | 45 | /******************* 46 | * Get the class name. 47 | * 48 | * @return The class name. 49 | ******************/ 50 | public String getClassName() { 51 | return this.className; 52 | } 53 | 54 | /******************* 55 | * Set the reference handle of the class. 56 | * 57 | * @param handle The reference handle value. 58 | ******************/ 59 | public void setHandle(int handle) { 60 | this.refHandle = handle; 61 | } 62 | 63 | /******************* 64 | * Get the reference handle. 65 | * 66 | * @return The reference handle value for this class. 67 | ******************/ 68 | public int getHandle() { 69 | return this.refHandle; 70 | } 71 | 72 | /******************* 73 | * Set the classDescFlags property. 74 | * 75 | * @param classDescFlags The classDescFlags value. 76 | ******************/ 77 | public void setClassDescFlags(byte classDescFlags) { 78 | this.classDescFlags = classDescFlags; 79 | } 80 | 81 | /******************* 82 | * Check whether the class is SC_SERIALIZABLE. 83 | * 84 | * @return True if the classDescFlags includes SC_SERIALIZABLE. 85 | ******************/ 86 | public boolean isSC_SERIALIZABLE() { 87 | return (this.classDescFlags & SC_SERIALIZABLE) == SC_SERIALIZABLE; 88 | } 89 | 90 | /******************* 91 | * Check whether the class is SC_EXTERNALIZABLE. 92 | * 93 | * @return True if the classDescFlags includes SC_EXTERNALIZABLE. 94 | ******************/ 95 | public boolean isSC_EXTERNALIZABLE() { 96 | return (this.classDescFlags & SC_EXTERNALIZABLE) == SC_EXTERNALIZABLE; 97 | } 98 | 99 | /******************* 100 | * Check whether the class is SC_WRITE_METHOD. 101 | * 102 | * @return True if the classDescFlags includes SC_WRITE_METHOD. 103 | ******************/ 104 | public boolean isSC_WRITE_METHOD() { 105 | return (this.classDescFlags & SC_WRITE_METHOD) == SC_WRITE_METHOD; 106 | } 107 | 108 | /******************* 109 | * Check whether the class is SC_BLOCKDATA. 110 | * 111 | * @return True if the classDescFlags includes SC_BLOCKDATA. 112 | ******************/ 113 | public boolean isSC_BLOCKDATA() { 114 | return (this.classDescFlags & SC_BLOCK_DATA) == SC_BLOCK_DATA; 115 | } 116 | 117 | /******************* 118 | * Add a field description to the class details object. 119 | * 120 | * @param cf The ClassField object describing the field. 121 | ******************/ 122 | public void addField(ClassField cf) { 123 | this.fieldDescriptions.add(cf); 124 | } 125 | 126 | /******************* 127 | * Get the class field descriptions. 128 | * 129 | * @return An array of field descriptions for the class. 130 | ******************/ 131 | public List getFields() { 132 | return this.fieldDescriptions; 133 | } 134 | 135 | /******************* 136 | * Set the name of the last field to be added to the ClassDetails object. 137 | * 138 | * @param name The field name. 139 | ******************/ 140 | public void setLastFieldName(String name) { 141 | this.fieldDescriptions.get(this.fieldDescriptions.size() - 1).setName(name); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/deser/support/ClassField.java: -------------------------------------------------------------------------------- 1 | package deser.support; 2 | 3 | /*********************************************************** 4 | * Support class for serialization data parsing that holds 5 | * details of a class field to enable the field value to 6 | * be read from the stream. 7 | *

8 | * Written by Nicky Bloor (@NickstaDB). 9 | **********************************************************/ 10 | public class ClassField { 11 | /** 12 | * The field type code 13 | */ 14 | private final byte typeCode; 15 | /** 16 | * The field name 17 | */ 18 | private String name; 19 | 20 | /******************* 21 | * Construct the ClassField object. 22 | * 23 | * @param typeCode The field type code. 24 | ******************/ 25 | public ClassField(byte typeCode) { 26 | this.typeCode = typeCode; 27 | this.name = ""; 28 | } 29 | 30 | /******************* 31 | * Get the field type code. 32 | * 33 | * @return The field type code. 34 | ******************/ 35 | public byte getTypeCode() { 36 | return this.typeCode; 37 | } 38 | 39 | /******************* 40 | * Set the field name. 41 | * 42 | * @param name The field name. 43 | ******************/ 44 | public void setName(String name) { 45 | this.name = name; 46 | } 47 | 48 | /******************* 49 | * Get the field name. 50 | * 51 | * @return The field name. 52 | ******************/ 53 | public String getName() { 54 | return this.name; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/cache/AliasingObjectInputStream.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.cache 2 | 3 | import java.io.InputStream 4 | import java.io.ObjectInputStream 5 | import java.io.ObjectStreamClass 6 | 7 | class AliasingObjectInputStream private constructor( 8 | inputStream: InputStream, 9 | private val aliases: Map>, 10 | ) : ObjectInputStream(inputStream) { 11 | constructor(inputStream: InputStream, block: MutableMap>.() -> Unit) : this(inputStream, buildMap(block)) 12 | 13 | override fun readClassDescriptor(): ObjectStreamClass { 14 | val baseDescriptor = super.readClassDescriptor() 15 | 16 | return if (aliases.containsKey(baseDescriptor.name)) { 17 | val aliasClassDescriptor = ObjectStreamClass.lookup(aliases[baseDescriptor.name]) 18 | val aliasUid = aliasClassDescriptor.serialVersionUID 19 | val expectedUid = baseDescriptor.serialVersionUID 20 | 21 | require(aliasUid == expectedUid) { "serialVersionUID mismatch; expected $expectedUid but got $aliasUid" } 22 | 23 | aliasClassDescriptor 24 | } else { 25 | baseDescriptor 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/cache/CacheEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.cache 2 | 3 | data class CacheEntry( 4 | val id: Int, 5 | val schemaId: Int, 6 | val schemaName: String, 7 | val timestamp: String, 8 | val attemptCount: Int, 9 | val dataCount: Int, 10 | ) 11 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/cache/CacheModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.cache 2 | 3 | import io.github.paulgriffith.kindling.utils.Column 4 | import io.github.paulgriffith.kindling.utils.ColumnList 5 | import org.jdesktop.swingx.renderer.DefaultTableRenderer 6 | import javax.swing.table.AbstractTableModel 7 | 8 | class CacheModel(private val entries: List) : AbstractTableModel() { 9 | override fun getColumnName(column: Int): String = CacheColumns[column].header 10 | override fun getRowCount(): Int = entries.size 11 | override fun getColumnCount(): Int = size 12 | override fun getValueAt(row: Int, column: Int): Any? = get(row, CacheColumns[column]) 13 | override fun getColumnClass(column: Int): Class<*> = CacheColumns[column].clazz 14 | 15 | operator fun get(row: Int, column: Column): T { 16 | return entries[row].let { info -> 17 | column.getValue(info) 18 | } 19 | } 20 | 21 | @Suppress("unused") 22 | companion object CacheColumns : ColumnList() { 23 | val Id by column( 24 | column = { 25 | cellRenderer = DefaultTableRenderer(Any?::toString) 26 | }, 27 | value = CacheEntry::id, 28 | ) 29 | val SchemaId by column { it.schemaId } 30 | val Timestamp by column { it.timestamp } 31 | val AttemptCount by column(name = "Attempt Count") { it.attemptCount } 32 | val DataCount by column(name = "Data Count") { it.dataCount } 33 | val SchemaName by column( 34 | column = { 35 | isVisible = false 36 | }, 37 | value = CacheEntry::schemaName, 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/cache/SchemaFilterList.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.cache 2 | 3 | import com.jidesoft.swing.CheckBoxList 4 | import io.github.paulgriffith.kindling.utils.listCellRenderer 5 | import java.awt.Font 6 | import java.awt.Font.MONOSPACED 7 | import javax.swing.AbstractListModel 8 | import javax.swing.DefaultListSelectionModel 9 | 10 | class SchemaModel(data: List) : AbstractListModel() { 11 | private val comparator: Comparator = compareBy(nullsFirst()) { it.id } 12 | private val values = data.sortedWith(comparator) 13 | 14 | override fun getSize(): Int { 15 | return values.size + 1 16 | } 17 | 18 | override fun getElementAt(index: Int): Any { 19 | return if (index == 0) { 20 | CheckBoxList.ALL_ENTRY 21 | } else { 22 | values[index - 1] 23 | } 24 | } 25 | } 26 | 27 | class SchemaFilterList(modelData: List) : CheckBoxList(SchemaModel(modelData)) { 28 | init { 29 | selectionModel = DefaultListSelectionModel() 30 | isClickInCheckBoxOnly = true 31 | visibleRowCount = 0 32 | 33 | val txGroupRegex = """(.*)\{.*}""".toRegex() 34 | 35 | cellRenderer = listCellRenderer { _, schemaEntry, _, _, _ -> 36 | text = when (schemaEntry) { 37 | is SchemaRecord -> { 38 | buildString { 39 | append("%4d".format(schemaEntry.id)) 40 | val name = txGroupRegex.find(schemaEntry.name)?.groups?.get(1)?.value ?: schemaEntry.name 41 | append(": ").append(name) 42 | 43 | when (val size = schemaEntry.errors.size) { 44 | 0 -> Unit 45 | 1 -> append(" ($size error. Click to view.)") 46 | else -> append(" ($size errors. Click to view.)") 47 | } 48 | } 49 | } 50 | else -> schemaEntry.toString() 51 | } 52 | font = Font(MONOSPACED, Font.PLAIN, 14) 53 | } 54 | selectAll() 55 | } 56 | 57 | override fun getModel() = super.getModel() as SchemaModel 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/cache/SchemaModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.cache 2 | 3 | data class SchemaRecord( 4 | val id: Int, 5 | val name: String, 6 | val errors: List, 7 | ) 8 | 9 | data class SchemaRow( 10 | val id: Int, 11 | val signature: String, 12 | val message: String?, 13 | ) 14 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/cache/model/AlarmJournalData.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.cache.model 2 | 3 | import com.inductiveautomation.ignition.common.alarming.EventData 4 | import com.inductiveautomation.ignition.common.alarming.evaluation.EventPropertyType 5 | import io.github.paulgriffith.kindling.core.Detail 6 | import java.io.Serializable 7 | import java.util.EnumSet 8 | 9 | class AlarmJournalData( 10 | private val profileName: String?, 11 | private val tableName: String?, 12 | private val dataTableName: String?, 13 | private val source: String?, 14 | private val dispPath: String?, 15 | private val uuid: String?, 16 | private val priority: Int, 17 | private val eventType: Int, 18 | private val eventFlags: Int, 19 | val data: EventData, 20 | private val storedProps: EnumSet, 21 | ) : Serializable { 22 | val details by lazy { 23 | mapOf( 24 | "profile" to profileName.toString(), 25 | "table" to tableName.toString(), 26 | "dataTable" to dataTableName.toString(), 27 | "source" to source.toString(), 28 | "displayPath" to dispPath.toString(), 29 | "uuid" to uuid.toString(), 30 | "priority" to priority.toString(), 31 | "eventType" to eventType.toString(), 32 | "eventFlags" to eventFlags.toString(), 33 | "storedProps" to storedProps.joinToString(), 34 | ) 35 | } 36 | 37 | val body by lazy { 38 | data.properties.map { property -> 39 | println(property.name) 40 | "${property.name} (${property.type.simpleName}) = ${data.getOrDefault(property)}" 41 | } 42 | } 43 | 44 | fun toDetail(): Detail { 45 | return Detail( 46 | title = "Alarm Journal Data", 47 | details = details, 48 | body = body, 49 | ) 50 | } 51 | 52 | companion object { 53 | @JvmStatic 54 | private val serialVersionUID = 1L 55 | } 56 | } 57 | 58 | class AlarmJournalSFGroup( 59 | private val groupId: String, 60 | private val entries: List, 61 | ) : Serializable { 62 | fun toDetail() = Detail( 63 | title = "Grouped Alarm Journal Data ($groupId)", 64 | details = entries.fold(mutableMapOf()) { acc, nextData -> 65 | acc.putAll(nextData.details) 66 | acc 67 | }, 68 | body = entries.flatMap { 69 | it.data.timestamp 70 | it.body 71 | }, 72 | ) 73 | 74 | companion object { 75 | @JvmStatic 76 | private val serialVersionUID = -1_199_203_578_454_144_713L 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/cache/model/AuditProfileData.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.cache.model 2 | 3 | import com.inductiveautomation.ignition.gateway.audit.AuditRecord 4 | import io.github.paulgriffith.kindling.core.Detail 5 | import java.io.Serializable 6 | 7 | @Suppress("unused") 8 | class AuditProfileData( 9 | private val auditRecord: AuditRecord, 10 | private val insertQuery: String, 11 | private val parentLog: String, 12 | ) : Serializable { 13 | fun toDetail() = Detail( 14 | title = "Audit Profile Data", 15 | message = insertQuery, 16 | body = mapOf( 17 | "actor" to auditRecord.actor, 18 | "action" to auditRecord.action, 19 | "actionValue" to auditRecord.actionValue, 20 | "actionTarget" to auditRecord.actionTarget, 21 | "actorHost" to auditRecord.actorHost, 22 | "originatingContext" to when (auditRecord.originatingContext) { 23 | 1 -> "Gateway" 24 | 2 -> "Designer" 25 | 4 -> "Client" 26 | else -> "Unknown" 27 | }, 28 | "originatingSystem" to auditRecord.originatingSystem, 29 | "timestamp" to auditRecord.timestamp.toString(), 30 | ).map { (key, value) -> 31 | "$key: $value" 32 | }, 33 | ) 34 | 35 | companion object { 36 | @JvmStatic 37 | private val serialVersionUID = 3037488986978918285L 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/cache/model/ScriptedSFData.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.cache.model 2 | 3 | import io.github.paulgriffith.kindling.core.Detail 4 | import java.io.Serializable 5 | 6 | @Suppress("unused") 7 | class ScriptedSFData( 8 | val query: String, 9 | val datasource: String, 10 | val values: Array, 11 | ) : Serializable { 12 | fun toDetail() = Detail( 13 | title = "system.db.runSFUpdate query data", 14 | message = query, 15 | details = mapOf( 16 | "datasource" to datasource, 17 | ), 18 | body = values.mapIndexed { index, parameterValue -> 19 | "param${index + 1} (${parameterValue?.javaClass?.simpleName}) = $parameterValue" 20 | }, 21 | ) 22 | 23 | companion object { 24 | @JvmStatic 25 | private val serialVersionUID = 1L 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/core/CustomIconView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.core 2 | 3 | import java.io.File 4 | import javax.swing.Icon 5 | import javax.swing.filechooser.FileView 6 | 7 | class CustomIconView : FileView() { 8 | override fun getIcon(file: File): Icon? = if (file.isFile) { 9 | Tool.byExtension[file.extension]?.icon?.derive(16, 16) 10 | } else { 11 | null 12 | } 13 | 14 | override fun getTypeDescription(file: File) = if (file.isFile) { 15 | Tool.byExtension[file.extension]?.description 16 | } else { 17 | null 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/core/Detail.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.core 2 | 3 | import io.github.paulgriffith.kindling.core.Detail.BodyLine 4 | 5 | data class Detail( 6 | val title: String, 7 | val message: String? = null, 8 | val details: Map = emptyMap(), 9 | val body: List = emptyList(), 10 | ) { 11 | data class BodyLine(val text: String, val link: String? = null) 12 | 13 | companion object { 14 | operator fun invoke( 15 | title: String, 16 | message: String? = null, 17 | details: Map = emptyMap(), 18 | body: List = emptyList(), 19 | ) = Detail( 20 | title, 21 | message, 22 | details, 23 | body.map(::BodyLine), 24 | ) 25 | } 26 | } 27 | 28 | fun MutableList.add(line: String, link: String? = null) { 29 | add(BodyLine(line, link)) 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/core/DetailsPane.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.core 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import com.formdev.flatlaf.extras.components.FlatTextPane 5 | import io.github.paulgriffith.kindling.internal.DetailsIcon 6 | import io.github.paulgriffith.kindling.utils.Action 7 | import io.github.paulgriffith.kindling.utils.FlatScrollPane 8 | import io.github.paulgriffith.kindling.utils.escapeHtml 9 | import net.miginfocom.swing.MigLayout 10 | import java.awt.Component 11 | import java.awt.Desktop 12 | import java.awt.EventQueue 13 | import java.awt.Rectangle 14 | import java.awt.Toolkit 15 | import java.awt.datatransfer.StringSelection 16 | import javax.swing.JButton 17 | import javax.swing.JFileChooser 18 | import javax.swing.JPanel 19 | import javax.swing.event.HyperlinkEvent 20 | import javax.swing.filechooser.FileNameExtensionFilter 21 | import javax.swing.text.ComponentView 22 | import javax.swing.text.Element 23 | import javax.swing.text.StyleConstants 24 | import javax.swing.text.View 25 | import javax.swing.text.ViewFactory 26 | import javax.swing.text.html.HTML 27 | import javax.swing.text.html.HTMLEditorKit 28 | import kotlin.properties.Delegates 29 | 30 | class DetailsPane(initialEvents: List = emptyList()) : JPanel(MigLayout("ins 0, fill")) { 31 | var events: List by Delegates.observable(initialEvents) { _, _, newValue -> 32 | textPane.text = newValue.toDisplayFormat() 33 | EventQueue.invokeLater { 34 | textPane.scrollRectToVisible(Rectangle(0, 0, 0, 0)) 35 | } 36 | } 37 | 38 | private val textPane = FlatTextPane().apply { 39 | isEditable = false 40 | editorKit = DetailsEditorKit() 41 | addHyperlinkListener { event -> 42 | if (event.eventType == HyperlinkEvent.EventType.ACTIVATED) { 43 | val desktop = Desktop.getDesktop() 44 | desktop.browse(event.url.toURI()) 45 | } 46 | } 47 | } 48 | 49 | private val copy = Action( 50 | description = "Copy to Clipboard", 51 | icon = FlatSVGIcon("icons/bx-clipboard.svg"), 52 | ) { 53 | val clipboard = Toolkit.getDefaultToolkit().systemClipboard 54 | clipboard.setContents(StringSelection(events.toClipboardFormat()), null) 55 | } 56 | 57 | private val save = Action( 58 | description = "Save to File", 59 | icon = FlatSVGIcon("icons/bx-save.svg"), 60 | ) { 61 | JFileChooser().apply { 62 | fileSelectionMode = JFileChooser.FILES_ONLY 63 | fileFilter = FileNameExtensionFilter("Text File", "txt") 64 | val save = showSaveDialog(this@DetailsPane) 65 | if (save == JFileChooser.APPROVE_OPTION) { 66 | selectedFile.writeText(events.toClipboardFormat()) 67 | } 68 | } 69 | } 70 | 71 | private val actionPanel = JPanel(MigLayout("flowy, top, ins 0")) 72 | 73 | val actions: MutableList = object : ArrayList() { 74 | init { 75 | add(copy) 76 | add(save) 77 | } 78 | 79 | override fun add(element: Action) = super.add(element).also { 80 | actionPanel.add( 81 | JButton(element).apply { 82 | hideActionText = true 83 | }, 84 | ) 85 | } 86 | } 87 | 88 | init { 89 | add(FlatScrollPane(textPane), "push, grow") 90 | add(actionPanel, "east") 91 | textPane.text = events.toDisplayFormat() 92 | } 93 | 94 | private fun List.toDisplayFormat(): String { 95 | return joinToString(separator = "", prefix = "") { event -> 96 | buildString { 97 | append("").append(event.title) 98 | if (event.details.isNotEmpty()) { 99 | append("  101 | "$detailPrefix$key = \"$value\"" 102 | } 103 | append("/>") 104 | } 105 | append("") 106 | if (event.message != null) { 107 | append("
") 108 | append(event.message.escapeHtml()) 109 | } 110 | if (event.body.isNotEmpty()) { 111 | event.body.joinTo(buffer = this, separator = "\n", prefix = "
", postfix = "
") { (text, link) -> 112 | if (link != null) { 113 | """$text""" 114 | } else { 115 | text 116 | } 117 | } 118 | } else { 119 | append("
") 120 | } 121 | } 122 | } 123 | } 124 | 125 | private fun List.toClipboardFormat(): String { 126 | return joinToString(separator = "\n\n") { event -> 127 | buildString { 128 | appendLine(event.title) 129 | if (event.message != null) { 130 | appendLine(event.message) 131 | } 132 | event.body.joinTo(buffer = this, separator = "\n") { "\t${it.text}" } 133 | } 134 | } 135 | } 136 | } 137 | 138 | private const val detailPrefix = "data-" 139 | 140 | class DetailsEditorKit : HTMLEditorKit() { 141 | init { 142 | styleSheet.apply { 143 | //language=CSS 144 | addRule( 145 | """ 146 | b { 147 | font-size: larger; 148 | } 149 | pre { 150 | font-size: 10px; 151 | } 152 | object { 153 | padding-left: 16px; 154 | } 155 | """.trimIndent(), 156 | ) 157 | } 158 | } 159 | 160 | override fun getViewFactory(): ViewFactory { 161 | return object : HTMLFactory() { 162 | override fun create(elem: Element): View { 163 | val attrs = elem.attributes 164 | val o = attrs.getAttribute(StyleConstants.NameAttribute) 165 | if (o == HTML.Tag.OBJECT) { 166 | return object : ComponentView(elem) { 167 | override fun createComponent(): Component { 168 | val details: Map = 169 | elem.attributes.attributeNames.asSequence() 170 | .filterIsInstance() 171 | .associate { rawAttribute -> 172 | rawAttribute.removePrefix(detailPrefix) to elem.attributes.getAttribute(rawAttribute) as String 173 | } 174 | return DetailsIcon(details) 175 | } 176 | } 177 | } 178 | return super.create(elem) 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/core/Kindling.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.core 2 | 3 | import com.formdev.flatlaf.FlatDarkLaf 4 | import com.formdev.flatlaf.FlatLaf 5 | import com.formdev.flatlaf.FlatLightLaf 6 | import com.formdev.flatlaf.extras.FlatAnimatedLafChange 7 | import com.formdev.flatlaf.themes.FlatMacDarkLaf 8 | import com.formdev.flatlaf.themes.FlatMacLightLaf 9 | import com.formdev.flatlaf.util.SystemInfo 10 | import com.jthemedetecor.OsThemeDetector 11 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea 12 | import org.jfree.chart.JFreeChart 13 | import java.awt.Image 14 | import java.awt.Toolkit 15 | import java.io.File 16 | import javax.swing.UIManager 17 | import kotlin.io.path.Path 18 | import kotlin.properties.Delegates 19 | import org.fife.ui.rsyntaxtextarea.Theme as RSyntaxTheme 20 | 21 | object Kindling { 22 | val homeLocation: File = Path(System.getProperty("user.home"), "Downloads").toFile() 23 | 24 | val frameIcon: Image = Toolkit.getDefaultToolkit().getImage(this::class.java.getResource("/icons/kindling.png")) 25 | 26 | @Suppress("ktlint:trailing-comma-on-declaration-site") 27 | enum class Theme(val lookAndFeel: FlatLaf, private val rSyntaxThemeName: String) { 28 | Light( 29 | lookAndFeel = if (SystemInfo.isMacOS) FlatMacLightLaf() else FlatLightLaf(), 30 | rSyntaxThemeName = "idea.xml", 31 | ), 32 | Dark( 33 | lookAndFeel = if (SystemInfo.isMacOS) FlatMacDarkLaf() else FlatDarkLaf(), 34 | rSyntaxThemeName = "dark.xml", 35 | ); 36 | 37 | private val rSyntaxTheme: RSyntaxTheme by lazy { 38 | RSyntaxTheme::class.java.getResourceAsStream("themes/$rSyntaxThemeName").use(org.fife.ui.rsyntaxtextarea.Theme::load) 39 | } 40 | 41 | fun apply(textArea: RSyntaxTextArea) { 42 | rSyntaxTheme.apply(textArea) 43 | } 44 | 45 | fun apply(chart: JFreeChart) { 46 | chart.xyPlot.apply { 47 | backgroundPaint = UIManager.getColor("Panel.background") 48 | domainAxis.tickLabelPaint = UIManager.getColor("ColorChooser.foreground") 49 | rangeAxis.tickLabelPaint = UIManager.getColor("ColorChooser.foreground") 50 | } 51 | chart.backgroundPaint = UIManager.getColor("Panel.background") 52 | } 53 | } 54 | 55 | private val themeListeners = mutableListOf<(Theme) -> Unit>() 56 | 57 | fun addThemeChangeListener(listener: (Theme) -> Unit) { 58 | themeListeners.add(listener) 59 | } 60 | 61 | private val themeDetector = OsThemeDetector.getDetector() 62 | 63 | var theme: Theme by Delegates.observable(if (themeDetector.isDark) Theme.Dark else Theme.Light) { _, _, newValue -> 64 | newValue.apply(true) 65 | for (listener in themeListeners) { 66 | listener.invoke(newValue) 67 | } 68 | } 69 | 70 | fun initTheme() { 71 | theme.apply(false) 72 | } 73 | 74 | private fun Theme.apply(animate: Boolean) { 75 | try { 76 | if (animate) { 77 | FlatAnimatedLafChange.showSnapshot() 78 | } 79 | UIManager.setLookAndFeel(lookAndFeel) 80 | FlatLaf.updateUI() 81 | } finally { 82 | // Will no-op if not animated 83 | FlatAnimatedLafChange.hideSnapshotWithAnimation() 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/core/Tool.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.core 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.paulgriffith.kindling.cache.CacheViewer 5 | import io.github.paulgriffith.kindling.idb.IdbViewer 6 | import io.github.paulgriffith.kindling.log.LogViewer 7 | import io.github.paulgriffith.kindling.thread.MultiThreadViewer 8 | import io.github.paulgriffith.kindling.utils.FileExtensionFilter 9 | import io.github.paulgriffith.kindling.utils.loadService 10 | import io.github.paulgriffith.kindling.zip.ZipViewer 11 | import java.io.File 12 | import java.nio.file.Path 13 | import javax.swing.filechooser.FileFilter 14 | 15 | interface Tool { 16 | val title: String 17 | val description: String 18 | val icon: FlatSVGIcon 19 | val extensions: List 20 | 21 | fun open(path: Path): ToolPanel 22 | 23 | val filter: FileExtensionFilter 24 | get() = FileExtensionFilter(description, extensions) 25 | 26 | companion object { 27 | val tools: List by lazy { 28 | listOf( 29 | ZipViewer, 30 | MultiThreadViewer, 31 | LogViewer, 32 | IdbViewer, 33 | CacheViewer, 34 | ) + loadService().sortedBy { it.title } 35 | } 36 | 37 | val byFilter: Map by lazy { 38 | tools.associateBy(Tool::filter) 39 | } 40 | 41 | val byExtension by lazy { 42 | buildMap { 43 | for (tool in tools) { 44 | for (extension in tool.extensions) { 45 | put(extension, tool) 46 | } 47 | } 48 | } 49 | } 50 | 51 | operator fun get(file: File): Tool { 52 | return checkNotNull( 53 | tools.find { tool -> 54 | tool.filter.accept(file) 55 | }, 56 | ) { "No tool found for $file" } 57 | } 58 | } 59 | } 60 | 61 | interface MultiTool : Tool { 62 | fun open(paths: List): ToolPanel 63 | 64 | override fun open(path: Path): ToolPanel = open(listOf(path)) 65 | } 66 | 67 | interface ClipboardTool : Tool { 68 | fun open(data: String): ToolPanel 69 | } 70 | 71 | class ToolOpeningException(message: String, cause: Throwable? = null) : Exception(message, cause) 72 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/core/ToolPanel.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.core 2 | 3 | import io.github.paulgriffith.kindling.utils.Action 4 | import io.github.paulgriffith.kindling.utils.FileExtensionFilter 5 | import io.github.paulgriffith.kindling.utils.FloatableComponent 6 | import io.github.paulgriffith.kindling.utils.PopupMenuCustomizer 7 | import io.github.paulgriffith.kindling.utils.Properties 8 | import io.github.paulgriffith.kindling.utils.exportToCSV 9 | import io.github.paulgriffith.kindling.utils.exportToXLSX 10 | import net.miginfocom.swing.MigLayout 11 | import java.io.File 12 | import javax.swing.Icon 13 | import javax.swing.JFileChooser 14 | import javax.swing.JMenu 15 | import javax.swing.JPanel 16 | import javax.swing.JPopupMenu 17 | import javax.swing.filechooser.FileFilter 18 | import javax.swing.table.TableModel 19 | 20 | abstract class ToolPanel( 21 | layoutConstraints: String = "ins 6, fill, hidemode 3", 22 | ) : JPanel(MigLayout(layoutConstraints)), FloatableComponent, PopupMenuCustomizer { 23 | abstract override val icon: Icon? 24 | override val tabName: String get() = name 25 | override val tabTooltip: String get() = toolTipText 26 | 27 | override fun customizePopupMenu(menu: JPopupMenu) = Unit 28 | 29 | protected fun exportMenu(defaultFileName: String = "", modelSupplier: () -> TableModel): JMenu = 30 | JMenu("Export").apply { 31 | for (format in ExportFormat.values()) { 32 | add( 33 | Action("Export as ${format.extension.uppercase()}") { 34 | exportFileChooser.apply { 35 | selectedFile = File(defaultFileName) 36 | resetChoosableFileFilters() 37 | fileFilter = format.fileFilter 38 | if (showSaveDialog(this@ToolPanel) == JFileChooser.APPROVE_OPTION) { 39 | val selectedFile = 40 | if (selectedFile.absolutePath.endsWith(format.extension)) { 41 | selectedFile 42 | } else { 43 | File(selectedFile.absolutePath + ".${format.extension}") 44 | } 45 | format.action.invoke(modelSupplier(), selectedFile) 46 | } 47 | } 48 | }, 49 | ) 50 | } 51 | } 52 | 53 | companion object { 54 | val exportFileChooser = JFileChooser(Kindling.homeLocation).apply { 55 | isMultiSelectionEnabled = false 56 | isAcceptAllFileFilterUsed = false 57 | fileView = CustomIconView() 58 | 59 | Kindling.addThemeChangeListener { 60 | updateUI() 61 | } 62 | } 63 | 64 | @Suppress("ktlint:trailing-comma-on-declaration-site") 65 | private enum class ExportFormat( 66 | description: String, 67 | val extension: String, 68 | val action: (TableModel, File) -> Unit, 69 | ) { 70 | CSV("Comma Separated Values", "csv", TableModel::exportToCSV), 71 | EXCEL("Excel Workbook", "xlsx", TableModel::exportToXLSX); 72 | 73 | val fileFilter: FileFilter = FileExtensionFilter(description, listOf(extension)) 74 | } 75 | 76 | val classMapsByVersion by lazy { 77 | val versions = requireNotNull(this::class.java.getResourceAsStream("/javadocs/versions.txt")).reader().readLines() 78 | versions.associateWith { version -> 79 | Properties(requireNotNull(this::class.java.getResourceAsStream("/javadocs/$version/links.properties"))) 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/IdbView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.paulgriffith.kindling.core.Tool 5 | import io.github.paulgriffith.kindling.core.ToolPanel 6 | import io.github.paulgriffith.kindling.idb.generic.GenericView 7 | import io.github.paulgriffith.kindling.idb.metrics.MetricsView 8 | import io.github.paulgriffith.kindling.log.Level 9 | import io.github.paulgriffith.kindling.log.LogPanel 10 | import io.github.paulgriffith.kindling.log.SystemLogsEvent 11 | import io.github.paulgriffith.kindling.utils.SQLiteConnection 12 | import io.github.paulgriffith.kindling.utils.TabStrip 13 | import io.github.paulgriffith.kindling.utils.toList 14 | import java.nio.file.Path 15 | import java.sql.Connection 16 | import java.time.Instant 17 | import kotlin.io.path.name 18 | 19 | class IdbView(path: Path) : ToolPanel() { 20 | private val connection = SQLiteConnection(path) 21 | 22 | private val tables: List = connection.metaData.getTables("", "", "", null).toList { rs -> 23 | rs.getString(3) 24 | } 25 | 26 | private val tabs = TabStrip().apply { 27 | trailingComponent = null 28 | isTabsClosable = false 29 | } 30 | 31 | init { 32 | name = path.name 33 | toolTipText = path.toString() 34 | 35 | tabs.addTab( 36 | tabName = "Tables", 37 | component = GenericView(connection), 38 | tabTooltip = null, 39 | select = true, 40 | ) 41 | 42 | var addedTabs = 0 43 | for (tool in IdbTool.values()) { 44 | if (tool.supports(tables)) { 45 | tabs.addLazyTab( 46 | tabName = tool.name, 47 | ) { 48 | tool.open(connection) 49 | } 50 | addedTabs += 1 51 | } 52 | } 53 | if (addedTabs == 1) { 54 | tabs.selectedIndex = tabs.indices.last 55 | } 56 | 57 | add(tabs, "push, grow") 58 | } 59 | 60 | override val icon = IdbViewer.icon 61 | 62 | override fun removeNotify() { 63 | super.removeNotify() 64 | connection.close() 65 | } 66 | } 67 | 68 | enum class IdbTool { 69 | @Suppress("SqlResolve") 70 | Logs { 71 | override fun supports(tables: List): Boolean = "logging_event" in tables 72 | override fun open(connection: Connection): ToolPanel { 73 | val stackTraces: Map> = connection.prepareStatement( 74 | //language=sql 75 | """ 76 | SELECT 77 | event_id, 78 | i, 79 | trace_line 80 | FROM 81 | logging_event_exception 82 | ORDER BY 83 | event_id, 84 | i 85 | """.trimIndent(), 86 | ).executeQuery() 87 | .toList { resultSet -> 88 | Pair( 89 | resultSet.getInt("event_id"), 90 | resultSet.getString("trace_line"), 91 | ) 92 | }.groupBy(keySelector = { it.first }, valueTransform = { it.second }) 93 | 94 | val mdcKeys: Map> = connection.prepareStatement( 95 | //language=sql 96 | """ 97 | SELECT 98 | event_id, 99 | mapped_key, 100 | mapped_value 101 | FROM 102 | logging_event_property 103 | ORDER BY 104 | event_id 105 | """.trimIndent(), 106 | ).executeQuery() 107 | .toList { resultSet -> 108 | Triple( 109 | resultSet.getInt("event_id"), 110 | resultSet.getString("mapped_key"), 111 | resultSet.getString("mapped_value"), 112 | ) 113 | }.groupingBy { it.first } 114 | .aggregateTo(mutableMapOf>()) { _, accumulator, element, _ -> 115 | val acc = accumulator ?: mutableMapOf() 116 | acc[element.second] = element.third ?: "null" 117 | acc 118 | } 119 | 120 | val events = connection.prepareStatement( 121 | //language=sql 122 | """ 123 | SELECT 124 | event_id, 125 | timestmp, 126 | formatted_message, 127 | logger_name, 128 | level_string, 129 | thread_name 130 | FROM 131 | logging_event 132 | ORDER BY 133 | event_id 134 | """.trimIndent(), 135 | ).executeQuery() 136 | .toList { resultSet -> 137 | val eventId = resultSet.getInt("event_id") 138 | SystemLogsEvent( 139 | timestamp = Instant.ofEpochMilli(resultSet.getLong("timestmp")), 140 | message = resultSet.getString("formatted_message"), 141 | logger = resultSet.getString("logger_name"), 142 | thread = resultSet.getString("thread_name"), 143 | level = Level.valueOf(resultSet.getString("level_string")), 144 | mdc = mdcKeys[eventId].orEmpty(), 145 | stacktrace = stackTraces[eventId].orEmpty(), 146 | ) 147 | } 148 | return LogPanel(events) 149 | } 150 | }, 151 | Metrics { 152 | override fun supports(tables: List): Boolean = "SYSTEM_METRICS" in tables 153 | override fun open(connection: Connection): ToolPanel = MetricsView(connection) 154 | }, 155 | // Images { 156 | // override fun supports(tables: List): Boolean = "IMAGES" in tables 157 | // override fun open(connection: Connection): ToolPanel = ImagesPanel(connection) 158 | // } 159 | ; 160 | 161 | abstract fun supports(tables: List): Boolean 162 | 163 | abstract fun open(connection: Connection): ToolPanel 164 | } 165 | 166 | object IdbViewer : Tool { 167 | override val title = "Idb File" 168 | override val description = ".idb (SQLite3) files" 169 | override val icon = FlatSVGIcon("icons/bx-hdd.svg") 170 | override val extensions = listOf("idb") 171 | override fun open(path: Path): ToolPanel = IdbView(path) 172 | } 173 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/ImagesTab.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb 2 | 3 | import com.inductiveautomation.ignition.gateway.images.ImageFormat 4 | import io.github.paulgriffith.kindling.core.ToolPanel 5 | import io.github.paulgriffith.kindling.utils.AbstractTreeNode 6 | import io.github.paulgriffith.kindling.utils.FlatScrollPane 7 | import io.github.paulgriffith.kindling.utils.TypedTreeNode 8 | import io.github.paulgriffith.kindling.utils.toList 9 | import io.github.paulgriffith.kindling.utils.treeCellRenderer 10 | import java.awt.Dimension 11 | import java.sql.Connection 12 | import javax.imageio.ImageIO 13 | import javax.swing.Icon 14 | import javax.swing.ImageIcon 15 | import javax.swing.JLabel 16 | import javax.swing.JTree 17 | import javax.swing.tree.DefaultTreeModel 18 | import javax.swing.tree.TreeSelectionModel 19 | 20 | class ImagesPanel(connection: Connection) : ToolPanel("ins 0, fill, hidemode 3") { 21 | override val icon: Icon? = null 22 | 23 | init { 24 | val tree = JTree(DefaultTreeModel(RootImageNode(connection))) 25 | tree.isRootVisible = false 26 | tree.cellRenderer = treeCellRenderer { _, value, _, _, _, _, _ -> 27 | when (value) { 28 | is ImageNode -> { 29 | text = value.userObject.path 30 | toolTipText = value.userObject.description 31 | } 32 | 33 | is ImageFolderNode -> { 34 | text = value.userObject 35 | } 36 | } 37 | this 38 | } 39 | 40 | add(FlatScrollPane(tree), "push, grow, w 30%!") 41 | val imageDisplay = JLabel() 42 | add(FlatScrollPane(imageDisplay), "push, grow") 43 | 44 | tree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION 45 | tree.addTreeSelectionListener { 46 | val node = it.newLeadSelectionPath?.lastPathComponent as? AbstractTreeNode 47 | imageDisplay.icon = if (node is ImageNode) { 48 | runCatching { 49 | val data = node.userObject.data 50 | val readers = ImageIO.getImageReadersByFormatName(node.userObject.type.name) 51 | val image = readers.asSequence().firstNotNullOfOrNull { reader -> 52 | ImageIO.createImageInputStream(data.inputStream())?.use { iis -> 53 | reader.input = iis 54 | reader.read( 55 | 0, 56 | reader.defaultReadParam.apply { 57 | sourceRenderSize = Dimension(200, 200) 58 | }, 59 | ) 60 | } 61 | } 62 | image?.let(::ImageIcon) 63 | }.getOrNull() 64 | } else { 65 | null 66 | } 67 | } 68 | } 69 | } 70 | 71 | private data class ImageNode(override val userObject: ImageRow) : TypedTreeNode() 72 | 73 | private data class ImageRow( 74 | val path: String, 75 | val type: ImageFormat, 76 | val description: String?, 77 | val data: ByteArray, 78 | ) { 79 | override fun equals(other: Any?): Boolean { 80 | if (this === other) return true 81 | if (javaClass != other?.javaClass) return false 82 | 83 | other as ImageRow 84 | 85 | if (path != other.path) return false 86 | if (type != other.type) return false 87 | if (description != other.description) return false 88 | if (!data.contentEquals(other.data)) return false 89 | 90 | return true 91 | } 92 | 93 | override fun hashCode(): Int { 94 | var result = path.hashCode() 95 | result = 31 * result + type.hashCode() 96 | result = 31 * result + (description?.hashCode() ?: 0) 97 | result = 31 * result + data.contentHashCode() 98 | return result 99 | } 100 | 101 | override fun toString(): String { 102 | return "ImageRow(path='$path', type=$type, description=$description)" 103 | } 104 | } 105 | 106 | private data class ImageFolderNode(override val userObject: String) : TypedTreeNode() 107 | 108 | class RootImageNode(connection: Connection) : AbstractTreeNode() { 109 | private val listAll = connection.prepareStatement( 110 | """ 111 | SELECT path, type, description, data 112 | FROM images 113 | WHERE type IS NOT NULL 114 | ORDER BY path 115 | """.trimIndent(), 116 | ) 117 | 118 | init { 119 | val images = listAll.use { 120 | it.executeQuery().toList { rs -> 121 | ImageRow( 122 | rs.getString("path"), 123 | rs.getString("type").let(ImageFormat::valueOf), 124 | rs.getString("description"), 125 | rs.getBytes("data"), 126 | ) 127 | } 128 | } 129 | 130 | val seen = mutableMapOf, AbstractTreeNode>() 131 | for (row in images) { 132 | var lastSeen: AbstractTreeNode = this 133 | val currentLeadingPath = mutableListOf() 134 | for (pathPart in row.path.split('/')) { 135 | currentLeadingPath.add(pathPart) 136 | val next = seen.getOrPut(currentLeadingPath.toList()) { 137 | val newChild = if (pathPart.contains('.')) { 138 | ImageNode(row) 139 | } else { 140 | ImageFolderNode(currentLeadingPath.joinToString("/")) 141 | } 142 | lastSeen.children.add(newChild) 143 | newChild 144 | } 145 | lastSeen = next 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/generic/Column.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.generic 2 | 3 | import java.util.Collections 4 | import java.util.Enumeration 5 | import javax.swing.tree.TreeNode 6 | 7 | data class Column( 8 | val name: String, 9 | val type: String, 10 | val notNull: Boolean, 11 | val defaultValue: String?, 12 | val primaryKey: Boolean, 13 | val hidden: Boolean, 14 | val _parent: () -> TreeNode, 15 | ) : TreeNode { 16 | override fun getChildAt(childIndex: Int): TreeNode? = null 17 | override fun getChildCount(): Int = 0 18 | override fun getParent(): TreeNode = _parent() 19 | override fun getIndex(node: TreeNode?): Int = -1 20 | override fun getAllowsChildren(): Boolean = false 21 | override fun isLeaf(): Boolean = true 22 | override fun children(): Enumeration = Collections.emptyEnumeration() 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/generic/DBMetaDataTree.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.generic 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import com.formdev.flatlaf.extras.components.FlatTree 5 | import com.jidesoft.swing.StyledLabelBuilder 6 | import com.jidesoft.swing.TreeSearchable 7 | import io.github.paulgriffith.kindling.utils.derive 8 | import io.github.paulgriffith.kindling.utils.treeCellRenderer 9 | import java.awt.Font 10 | import javax.swing.UIManager 11 | import javax.swing.tree.TreeModel 12 | import javax.swing.tree.TreePath 13 | import javax.swing.tree.TreeSelectionModel 14 | 15 | class DBMetaDataTree(treeModel: TreeModel) : FlatTree() { 16 | init { 17 | model = treeModel 18 | isRootVisible = false 19 | setShowsRootHandles(true) 20 | selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION 21 | setCellRenderer( 22 | treeCellRenderer { _, value, selected, _, _, _, focused -> 23 | when (value) { 24 | is Table -> { 25 | text = value.name 26 | icon = if (selected && focused) TABLE_ICON_SELECTED else TABLE_ICON 27 | this 28 | } 29 | 30 | is Column -> { 31 | StyledLabelBuilder() 32 | .add(value.name) 33 | .add(" ") 34 | .add(value.type.takeIf { it.isNotEmpty() } ?: "UNKNOWN", Font.ITALIC) 35 | .createLabel() 36 | .apply { 37 | icon = if (selected && focused) COLUMN_ICON_SELECTED else COLUMN_ICON 38 | foreground = when { 39 | selected && focused -> UIManager.getColor("Tree.selectionForeground") 40 | selected -> UIManager.getColor("Tree.selectionInactiveForeground") 41 | else -> UIManager.getColor("Tree.textForeground") 42 | } 43 | } 44 | } 45 | 46 | else -> this 47 | } 48 | }, 49 | ) 50 | 51 | object : TreeSearchable(this) { 52 | init { 53 | isRecursive = true 54 | isRepeats = true 55 | } 56 | 57 | override fun convertElementToString(element: Any?): String { 58 | return when (val node = (element as? TreePath)?.lastPathComponent) { 59 | is Table -> node.name 60 | is Column -> node.name 61 | else -> "" 62 | } 63 | } 64 | } 65 | } 66 | 67 | companion object { 68 | private val TABLE_ICON = FlatSVGIcon("icons/bx-table.svg").derive(0.75F) 69 | private val TABLE_ICON_SELECTED = TABLE_ICON.derive { UIManager.getColor("Tree.selectionForeground") } 70 | 71 | private val COLUMN_ICON = FlatSVGIcon("icons/bx-column.svg").derive(0.75F) 72 | private val COLUMN_ICON_SELECTED = COLUMN_ICON.derive { UIManager.getColor("Tree.selectionForeground") } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/generic/GenericView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.generic 2 | 3 | import io.github.paulgriffith.kindling.core.ToolPanel 4 | import io.github.paulgriffith.kindling.utils.Action 5 | import io.github.paulgriffith.kindling.utils.FlatScrollPane 6 | import io.github.paulgriffith.kindling.utils.attachPopupMenu 7 | import io.github.paulgriffith.kindling.utils.javaType 8 | import io.github.paulgriffith.kindling.utils.toList 9 | import net.miginfocom.swing.MigLayout 10 | import java.awt.Dimension 11 | import java.awt.Toolkit 12 | import java.awt.event.KeyEvent 13 | import java.sql.Connection 14 | import java.sql.JDBCType 15 | import java.sql.Timestamp 16 | import java.util.Collections 17 | import java.util.Enumeration 18 | import javax.swing.Icon 19 | import javax.swing.JButton 20 | import javax.swing.JMenuItem 21 | import javax.swing.JPanel 22 | import javax.swing.JPopupMenu 23 | import javax.swing.JSplitPane 24 | import javax.swing.JTextArea 25 | import javax.swing.KeyStroke 26 | import javax.swing.tree.DefaultTreeModel 27 | import javax.swing.tree.TreeNode 28 | 29 | class GenericView(connection: Connection) : ToolPanel("ins 0, fill, hidemode 3") { 30 | private val tables: List = connection 31 | .prepareStatement("SELECT name FROM main.sqlite_schema WHERE type = \"table\" ORDER BY name") 32 | .executeQuery() 33 | .toList { resultSet -> 34 | resultSet.getString(1) 35 | }.mapIndexed { i, tableName -> 36 | Table( 37 | name = tableName, 38 | _parent = { root }, 39 | columns = connection 40 | .prepareStatement("PRAGMA table_xinfo(\"$tableName\");") 41 | .executeQuery() 42 | .toList { resultSet -> 43 | Column( 44 | name = resultSet.getString("name"), 45 | type = resultSet.getString("type"), 46 | notNull = resultSet.getInt("notnull") == 1, 47 | defaultValue = resultSet.getString("dflt_value"), 48 | primaryKey = resultSet.getInt("pk") == 1, 49 | hidden = resultSet.getInt("hidden") == 1, 50 | _parent = { root.getChildAt(i) }, 51 | ) 52 | }, 53 | ) 54 | } 55 | 56 | private val root: TreeNode = object : TreeNode { 57 | override fun getChildAt(childIndex: Int): TreeNode = tables[childIndex] 58 | override fun getChildCount(): Int = tables.size 59 | override fun getParent(): TreeNode? = null 60 | override fun getIndex(node: TreeNode): Int = tables.indexOf(node) 61 | override fun getAllowsChildren(): Boolean = true 62 | override fun isLeaf(): Boolean = false 63 | override fun children(): Enumeration = Collections.enumeration(tables) 64 | } 65 | 66 | private val tree = DBMetaDataTree(DefaultTreeModel(root)) 67 | 68 | private val query = JTextArea(0, 0) 69 | 70 | private val execute = Action(name = "Execute") { 71 | results.result = if (!query.text.isNullOrEmpty()) { 72 | try { 73 | connection.prepareStatement(query.text) 74 | .executeQuery() 75 | .use { resultSet -> 76 | val columnCount = resultSet.metaData.columnCount 77 | val names = List(columnCount) { i -> resultSet.metaData.getColumnName(i + 1) } 78 | val types = List(columnCount) { i -> 79 | val timestamp = TIMESTAMP_COLUMN_NAMES.any { 80 | resultSet.metaData.getColumnName(i + 1).contains(it, true) 81 | } 82 | 83 | if (timestamp) { 84 | Timestamp::class.java 85 | } else { 86 | val sqlType = resultSet.metaData.getColumnType(i + 1) 87 | val jdbcType = JDBCType.valueOf(sqlType) 88 | jdbcType.javaType 89 | } 90 | } 91 | 92 | val data = resultSet.toList { 93 | List(columnCount) { i -> 94 | // SQLite stores booleans as ints, we'll use actual booleans to make things easier 95 | if (types[i] == Boolean::class.javaObjectType) { 96 | resultSet.getObject(i + 1) == 1 97 | } else { 98 | resultSet.getObject(i + 1) 99 | } 100 | } 101 | } 102 | 103 | QueryResult.Success(names, types, data) 104 | } 105 | } catch (e: Exception) { 106 | QueryResult.Error(e.message ?: "Error") 107 | } 108 | } else { 109 | QueryResult.Error("Enter a query in the text field above") 110 | } 111 | } 112 | 113 | private val queryPanel = JPanel(MigLayout("ins 0, fill")).apply { 114 | add(JButton(execute), "wrap") 115 | add(query, "push, grow") 116 | } 117 | 118 | private val results = ResultsPanel() 119 | 120 | init { 121 | val ctrlEnter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx) 122 | getInputMap(WHEN_IN_FOCUSED_WINDOW).put(ctrlEnter, "execute") 123 | actionMap.put("execute", execute) 124 | 125 | tree.attachPopupMenu { event -> 126 | val path = getClosestPathForLocation(event.x, event.y) 127 | when (val node = path?.lastPathComponent) { 128 | is Table -> JPopupMenu().apply { 129 | add( 130 | JMenuItem( 131 | Action("SELECT * FROM ${node.name}") { 132 | query.text = "SELECT * FROM ${node.name};" 133 | }, 134 | ), 135 | ) 136 | } 137 | 138 | is Column -> JPopupMenu().apply { 139 | val table = path.parentPath.lastPathComponent as Table 140 | add( 141 | JMenuItem( 142 | Action("SELECT ${node.name} FROM ${table.name}") { 143 | query.text = "SELECT ${node.name} FROM ${table.name}" 144 | }, 145 | ), 146 | ) 147 | } 148 | 149 | else -> null 150 | } 151 | } 152 | 153 | add( 154 | JSplitPane( 155 | JSplitPane.HORIZONTAL_SPLIT, 156 | FlatScrollPane(tree).apply { 157 | preferredSize = Dimension(200, 10) 158 | }, 159 | JSplitPane( 160 | JSplitPane.VERTICAL_SPLIT, 161 | FlatScrollPane(queryPanel), 162 | results, 163 | ).apply { 164 | resizeWeight = 0.2 165 | }, 166 | ).apply { 167 | resizeWeight = 0.1 168 | }, 169 | "push, grow", 170 | ) 171 | } 172 | 173 | override val icon: Icon? = null 174 | 175 | companion object { 176 | private val TIMESTAMP_COLUMN_NAMES = setOf("timestamp", "timestmp", "t_stamp", "tstamp") 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/generic/QueryResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.generic 2 | 3 | import javax.swing.table.AbstractTableModel 4 | 5 | sealed interface QueryResult { 6 | class Success( 7 | val columnNames: List, 8 | private val columnTypes: List>, 9 | val data: List>, 10 | ) : QueryResult, AbstractTableModel() { 11 | constructor() : this(emptyList(), emptyList(), emptyList()) 12 | 13 | init { 14 | require(columnNames.size == columnTypes.size) 15 | } 16 | 17 | override fun getRowCount(): Int = data.size 18 | override fun getColumnCount(): Int = columnNames.size 19 | override fun getColumnName(columnIndex: Int): String = columnNames[columnIndex] 20 | override fun getColumnClass(columnIndex: Int): Class<*> = columnTypes[columnIndex] 21 | override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? = data[rowIndex][columnIndex] 22 | } 23 | 24 | class Error( 25 | val details: String, 26 | ) : QueryResult 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/generic/ResultsPanel.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.generic 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import com.inductiveautomation.ignition.common.util.csv.CSVWriter 5 | import io.github.paulgriffith.kindling.utils.Action 6 | import io.github.paulgriffith.kindling.utils.FlatScrollPane 7 | import io.github.paulgriffith.kindling.utils.ReifiedJXTable 8 | import io.github.paulgriffith.kindling.utils.ReifiedLabelProvider.Companion.setDefaultRenderer 9 | import io.github.paulgriffith.kindling.utils.selectedOrAllRowIndices 10 | import io.github.paulgriffith.kindling.utils.toFileSizeLabel 11 | import net.miginfocom.swing.MigLayout 12 | import java.awt.Toolkit 13 | import java.awt.datatransfer.StringSelection 14 | import java.io.File 15 | import java.util.Base64 16 | import javax.swing.JButton 17 | import javax.swing.JFileChooser 18 | import javax.swing.JLabel 19 | import javax.swing.JPanel 20 | import javax.swing.filechooser.FileNameExtensionFilter 21 | 22 | class ResultsPanel : JPanel(MigLayout("ins 0, fill, hidemode 3")) { 23 | private val table = ReifiedJXTable(QueryResult.Success()).apply { 24 | setDefaultRenderer( 25 | getText = { 26 | if (it != null) { 27 | "${it.size.toLong().toFileSizeLabel()} BLOB" 28 | } else { 29 | "" 30 | } 31 | }, 32 | getTooltip = { "Export to CSV to view full data (b64 encoded)" }, 33 | ) 34 | } 35 | 36 | private val errorDisplay = JLabel("No results - run a query in the text area above") 37 | 38 | private val tableDisplay = FlatScrollPane(table).apply { 39 | isVisible = false 40 | } 41 | 42 | var result: QueryResult? = null 43 | set(value) { 44 | when (value) { 45 | is QueryResult.Success -> { 46 | table.model = value 47 | tableDisplay.isVisible = true 48 | errorDisplay.isVisible = false 49 | copy.isEnabled = value.rowCount > 0 50 | save.isEnabled = value.rowCount > 0 51 | } 52 | 53 | is QueryResult.Error -> { 54 | errorDisplay.text = value.details 55 | errorDisplay.icon = ERROR_ICON 56 | tableDisplay.isVisible = false 57 | errorDisplay.isVisible = true 58 | } 59 | 60 | else -> Unit 61 | } 62 | field = value 63 | } 64 | 65 | private val copy = Action( 66 | description = "Copy to Clipboard", 67 | icon = FlatSVGIcon("icons/bx-clipboard.svg"), 68 | ) { 69 | val tsv = buildString { 70 | table.model.columnNames.joinTo(buffer = this, separator = "\t") 71 | appendLine() 72 | val rowsToExport = table.selectedOrAllRowIndices() 73 | rowsToExport.map { table.model.data[it] } 74 | .forEach { line -> 75 | line.joinTo(buffer = this, separator = "\t") { cell -> 76 | when (cell) { 77 | is ByteArray -> BASE64.encodeToString(cell) 78 | else -> cell?.toString().orEmpty() 79 | } 80 | } 81 | appendLine() 82 | } 83 | } 84 | 85 | val clipboard = Toolkit.getDefaultToolkit().systemClipboard 86 | clipboard.setContents(StringSelection(tsv), null) 87 | } 88 | 89 | private val save = Action( 90 | description = "Save to File", 91 | icon = FlatSVGIcon("icons/bx-save.svg"), 92 | ) { 93 | JFileChooser().apply { 94 | fileSelectionMode = JFileChooser.FILES_ONLY 95 | fileFilter = FileNameExtensionFilter("CSV File", "csv") 96 | selectedFile = File("query results.csv") 97 | val save = showSaveDialog(this@ResultsPanel) 98 | if (save == JFileChooser.APPROVE_OPTION) { 99 | CSVWriter(selectedFile.writer()).use { csv -> 100 | csv.writeNext(table.model.columnNames) 101 | val rowsToExport = table.selectedOrAllRowIndices() 102 | rowsToExport.map { table.model.data[it] } 103 | .forEach { line -> 104 | csv.writeNext( 105 | line.map { cell -> 106 | when (cell) { 107 | is ByteArray -> BASE64.encodeToString(cell) 108 | else -> cell?.toString() 109 | } 110 | }, 111 | ) 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | init { 119 | add(errorDisplay, "cell 0 0, push, grow") 120 | add(tableDisplay, "cell 0 0, push, grow") 121 | add(JButton(copy), "cell 1 0, top, flowy") 122 | add(JButton(save), "cell 1 0") 123 | } 124 | 125 | companion object { 126 | private val BASE64: Base64.Encoder = Base64.getEncoder() 127 | private val ERROR_ICON = FlatSVGIcon("icons/bx-error.svg").derive(3.0F) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/generic/Table.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.generic 2 | 3 | import java.util.Collections 4 | import java.util.Enumeration 5 | import javax.swing.tree.TreeNode 6 | 7 | data class Table( 8 | val name: String, 9 | val columns: List, 10 | val _parent: () -> TreeNode, 11 | ) : TreeNode { 12 | override fun getChildAt(childIndex: Int): TreeNode = columns[childIndex] 13 | override fun getChildCount(): Int = columns.size 14 | override fun getParent(): TreeNode = _parent() 15 | override fun getIndex(node: TreeNode): Int = columns.indexOf(node) 16 | override fun getAllowsChildren(): Boolean = true 17 | override fun isLeaf(): Boolean = false 18 | override fun children(): Enumeration = Collections.enumeration(columns) 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/metrics/Metric.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.metrics 2 | 3 | import java.util.Date 4 | 5 | @JvmInline 6 | value class Metric(val name: String) 7 | 8 | data class MetricData(val value: Double, val timestamp: Date) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/metrics/MetricCard.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.metrics 2 | 3 | import io.github.paulgriffith.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Cpu 4 | import io.github.paulgriffith.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Default 5 | import io.github.paulgriffith.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Heap 6 | import io.github.paulgriffith.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Queue 7 | import io.github.paulgriffith.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Throughput 8 | import io.github.paulgriffith.kindling.utils.Action 9 | import io.github.paulgriffith.kindling.utils.jFrame 10 | import net.miginfocom.swing.MigLayout 11 | import org.jdesktop.swingx.border.DropShadowBorder 12 | import org.jfree.chart.ChartPanel 13 | import org.jfree.chart.annotations.XYLineAnnotation 14 | import org.jfree.data.statistics.Regression 15 | import org.jfree.data.xy.XYDataset 16 | import java.awt.BasicStroke 17 | import java.awt.Font 18 | import java.text.DecimalFormat 19 | import java.text.FieldPosition 20 | import java.text.NumberFormat 21 | import java.text.ParsePosition 22 | import java.text.SimpleDateFormat 23 | import javax.swing.JLabel 24 | import javax.swing.JPanel 25 | import javax.swing.SwingConstants.CENTER 26 | import javax.swing.UIManager 27 | 28 | class MetricCard(val metric: Metric, data: List) : JPanel(MigLayout("fill, ins 10")) { 29 | private val presentation = metric.presentation 30 | 31 | private val sparkLine = ChartPanel( 32 | /* chart = */ sparkline(data, presentation.formatter), 33 | /* properties = */ false, 34 | /* save = */ false, 35 | /* print = */ false, 36 | /* zoom = */ true, 37 | /* tooltips = */ true, 38 | ).apply { 39 | popupMenu.addSeparator() 40 | popupMenu.add( 41 | Action("Popout") { 42 | jFrame( 43 | title = metric.name, 44 | width = 800, 45 | height = 600, 46 | ) { 47 | add( 48 | ChartPanel( 49 | sparkline( 50 | data, 51 | presentation.formatter, 52 | ), 53 | ), 54 | ) 55 | } 56 | }, 57 | ) 58 | } 59 | 60 | init { 61 | add( 62 | JLabel(metric.name, CENTER).apply { 63 | font = font.deriveFont(Font.BOLD, 14.0F) 64 | }, 65 | "span, pushx, growx", 66 | ) 67 | 68 | val aggregateData = DoubleArray(data.size) { i -> data[i].value } 69 | add(JLabel("Min: ${presentation.formatter.format(aggregateData.min())}", CENTER), "pushx, growx") 70 | add(JLabel("Avg: ${presentation.formatter.format(aggregateData.average())}", CENTER), "pushx, growx") 71 | add(JLabel("Max: ${presentation.formatter.format(aggregateData.max())}", CENTER), "pushx, growx, wrap") 72 | 73 | val minTimestamp = data.first().timestamp 74 | val maxTimestamp = data.last().timestamp 75 | 76 | if (presentation.isShowTrend) { 77 | val regression = regressionFunction(sparkLine.chart.xyPlot.dataset, 0) 78 | val minTimeDouble = minTimestamp.time.toDouble() 79 | val maxTimeDouble = maxTimestamp.time.toDouble() 80 | 81 | sparkLine.chart.xyPlot.addAnnotation( 82 | XYLineAnnotation( 83 | minTimeDouble, 84 | regression(minTimeDouble), 85 | maxTimeDouble, 86 | regression(maxTimeDouble), 87 | BasicStroke(1.0f), 88 | UIManager.getColor("Actions.Yellow"), 89 | ), 90 | ) 91 | } 92 | 93 | add(sparkLine, "span, w 300, h 170, pushx, growx") 94 | add(JLabel("${DATE_FORMAT.format(minTimestamp)} - ${DATE_FORMAT.format(maxTimestamp)}", CENTER), "pushx, growx, span") 95 | 96 | border = DropShadowBorder().apply { 97 | isShowRightShadow = true 98 | isShowBottomShadow = true 99 | shadowSize = 10 100 | } 101 | } 102 | 103 | companion object { 104 | val DATE_FORMAT = SimpleDateFormat("MM/dd/yy HH:mm:ss") 105 | 106 | private val mbFormatter = DecimalFormat("0.0 'mB'") 107 | private val heapFormatter = object : NumberFormat() { 108 | override fun format(number: Double, toAppendTo: StringBuffer, pos: FieldPosition): StringBuffer { 109 | return mbFormatter.format(number / 1_000_000, toAppendTo, pos) 110 | } 111 | 112 | override fun format(number: Long, toAppendTo: StringBuffer, pos: FieldPosition): StringBuffer = mbFormatter.format(number, toAppendTo, pos) 113 | override fun parse(source: String, parsePosition: ParsePosition): Number = mbFormatter.parse(source, parsePosition) 114 | } 115 | 116 | @Suppress("ktlint:trailing-comma-on-declaration-site") 117 | enum class MetricPresentation(val formatter: NumberFormat, val isShowTrend: Boolean) { 118 | Heap(heapFormatter, true), 119 | Queue(NumberFormat.getIntegerInstance(), false), 120 | Throughput(DecimalFormat("0.00 'dp/s'"), true), 121 | Cpu( 122 | DecimalFormat("0.00%").apply { 123 | multiplier = 1 124 | }, 125 | true, 126 | ), 127 | Default(NumberFormat.getInstance(), false); 128 | } 129 | 130 | private val Metric.presentation: MetricPresentation 131 | get() = when { 132 | name.contains("heap", true) -> Heap 133 | name.contains("queue", true) -> Queue 134 | name.contains("throughput", true) -> Throughput 135 | name.contains("cpu", true) -> Cpu 136 | else -> Default 137 | } 138 | 139 | fun regressionFunction(dataset: XYDataset, series: Int): (Double) -> Double { 140 | val (a, b) = Regression.getOLSRegression(dataset, series) 141 | return { x -> 142 | a + b * x 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/metrics/MetricTree.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.metrics 2 | 3 | import com.jidesoft.swing.CheckBoxTree 4 | import io.github.paulgriffith.kindling.utils.AbstractTreeNode 5 | import io.github.paulgriffith.kindling.utils.TypedTreeNode 6 | import io.github.paulgriffith.kindling.utils.treeCellRenderer 7 | import javax.swing.tree.DefaultTreeModel 8 | import javax.swing.tree.TreeNode 9 | import javax.swing.tree.TreePath 10 | 11 | data class MetricNode(override val userObject: List) : TypedTreeNode>() { 12 | constructor(vararg parts: String) : this(parts.toList()) 13 | 14 | val name by lazy { userObject.joinToString(".") } 15 | } 16 | 17 | class RootNode(metrics: List) : AbstractTreeNode() { 18 | init { 19 | val legacy = MetricNode("Legacy") 20 | val modern = MetricNode("New") 21 | 22 | val seen = mutableMapOf, MetricNode>() 23 | for (metric in metrics) { 24 | var lastSeen = if (metric.isLegacy) legacy else modern 25 | val currentLeadingPath = mutableListOf(lastSeen.name) 26 | for (part in metric.name.split('.')) { 27 | currentLeadingPath.add(part) 28 | val next = seen.getOrPut(currentLeadingPath.toList()) { 29 | val newChild = MetricNode(currentLeadingPath.drop(1)) 30 | lastSeen.children.add(newChild) 31 | newChild 32 | } 33 | lastSeen = next 34 | } 35 | } 36 | 37 | when { 38 | legacy.childCount == 0 && modern.childCount > 0 -> { 39 | for (zoomer in modern.children) { 40 | children.add(zoomer) 41 | } 42 | } 43 | 44 | modern.childCount == 0 && legacy.childCount > 0 -> { 45 | for (boomer in legacy.children) { 46 | children.add(boomer) 47 | } 48 | } 49 | 50 | else -> { 51 | children.add(legacy) 52 | children.add(modern) 53 | } 54 | } 55 | } 56 | 57 | private val Metric.isLegacy: Boolean 58 | get() = name.first().isUpperCase() 59 | } 60 | 61 | class MetricModel(metrics: List) : DefaultTreeModel(RootNode(metrics)) 62 | 63 | class MetricTree(metrics: List) : CheckBoxTree(MetricModel(metrics)) { 64 | init { 65 | isRootVisible = false 66 | setShowsRootHandles(true) 67 | 68 | expandAll() 69 | selectAll() 70 | 71 | setCellRenderer( 72 | treeCellRenderer { _, value, _, _, _, _, _ -> 73 | if (value is MetricNode) { 74 | val path = value.userObject 75 | text = path.last() 76 | toolTipText = value.name 77 | } 78 | this 79 | }, 80 | ) 81 | } 82 | 83 | val selectedLeafNodes: List 84 | get() = checkBoxTreeSelectionModel.selectionPaths 85 | .flatMap { 86 | (it.lastPathComponent as AbstractTreeNode).depthFirstChildren() 87 | }.filterIsInstance() 88 | 89 | private fun TreeNode.depthFirstChildren(): Sequence = sequence { 90 | if (isLeaf) { 91 | yield(this@depthFirstChildren as AbstractTreeNode) 92 | } else { 93 | for (child in children()) { 94 | yieldAll(child.depthFirstChildren()) 95 | } 96 | } 97 | } 98 | 99 | private fun expandAll() { 100 | var i = 0 101 | while (i < rowCount) { 102 | expandRow(i) 103 | i += 1 104 | } 105 | } 106 | 107 | private fun selectAll() = checkBoxTreeSelectionModel.addSelectionPath(TreePath(model.root)) 108 | } 109 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/metrics/MetricsView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.metrics 2 | 3 | import io.github.paulgriffith.kindling.core.ToolPanel 4 | import io.github.paulgriffith.kindling.utils.EDT_SCOPE 5 | import io.github.paulgriffith.kindling.utils.FlatScrollPane 6 | import io.github.paulgriffith.kindling.utils.toList 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.launch 10 | import net.miginfocom.swing.MigLayout 11 | import java.sql.Connection 12 | import javax.swing.Icon 13 | import javax.swing.JPanel 14 | 15 | class MetricsView(connection: Connection) : ToolPanel("ins 0, fill, hidemode 3") { 16 | private val metrics: List = connection.prepareStatement( 17 | //language=sql 18 | """ 19 | SELECT DISTINCT 20 | METRIC_NAME 21 | FROM SYSTEM_METRICS 22 | """, 23 | ).executeQuery().toList { rs -> 24 | Metric(rs.getString(1)) 25 | } 26 | 27 | private val metricTree = MetricTree(metrics) 28 | 29 | private val metricDataQuery = connection.prepareStatement( 30 | //language=sql 31 | """ 32 | SELECT DISTINCT 33 | VALUE, 34 | TIMESTAMP 35 | FROM SYSTEM_METRICS 36 | WHERE METRIC_NAME = ? 37 | ORDER BY TIMESTAMP 38 | """, 39 | ) 40 | 41 | private val metricCards: List = metrics.map { metric -> 42 | val metricData = metricDataQuery.apply { 43 | setString(1, metric.name) 44 | } 45 | .executeQuery() 46 | .toList { rs -> 47 | MetricData(rs.getDouble(1), rs.getDate(2)) 48 | } 49 | 50 | MetricCard(metric, metricData) 51 | } 52 | 53 | private val cardPanel = JPanel(MigLayout("wrap 3, fillx, hidemode 3")).apply { 54 | for (card in metricCards) { 55 | add(card, "pushx, growx") 56 | } 57 | } 58 | 59 | init { 60 | add(FlatScrollPane(metricTree), "grow, w 200::20%") 61 | add(FlatScrollPane(cardPanel), "push, grow, span") 62 | 63 | metricTree.checkBoxTreeSelectionModel.addTreeSelectionListener { updateData() } 64 | } 65 | 66 | private fun updateData() { 67 | BACKGROUND.launch { 68 | val selectedMetricNames = metricTree.selectedLeafNodes.map { it.name } 69 | EDT_SCOPE.launch { 70 | for (card in metricCards) { 71 | card.isVisible = card.metric.name in selectedMetricNames 72 | } 73 | } 74 | } 75 | } 76 | 77 | override val icon: Icon? = null 78 | 79 | companion object { 80 | private val BACKGROUND = CoroutineScope(Dispatchers.Default) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/idb/metrics/Sparkline.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.idb.metrics 2 | 3 | import io.github.paulgriffith.kindling.core.Kindling 4 | import io.github.paulgriffith.kindling.idb.metrics.MetricCard.Companion.DATE_FORMAT 5 | import org.jfree.chart.ChartFactory 6 | import org.jfree.chart.JFreeChart 7 | import org.jfree.chart.axis.NumberAxis 8 | import org.jfree.chart.ui.RectangleInsets 9 | import org.jfree.data.time.FixedMillisecond 10 | import org.jfree.data.time.TimeSeries 11 | import org.jfree.data.time.TimeSeriesCollection 12 | import java.text.NumberFormat 13 | 14 | fun sparkline(data: List, formatter: NumberFormat): JFreeChart { 15 | return ChartFactory.createTimeSeriesChart( 16 | /* title = */ null, 17 | /* timeAxisLabel = */ null, 18 | /* valueAxisLabel = */ null, 19 | /* dataset = */ 20 | TimeSeriesCollection( 21 | TimeSeries("Series").apply { 22 | for ((value, timestamp) in data) { 23 | add(FixedMillisecond(timestamp), value, false) 24 | } 25 | }, 26 | ), 27 | /* legend = */ false, 28 | /* tooltips = */ true, 29 | /* urls = */ false, 30 | ).apply { 31 | xyPlot.apply { 32 | domainAxis.isPositiveArrowVisible = true 33 | rangeAxis.apply { 34 | isPositiveArrowVisible = true 35 | (this as NumberAxis).numberFormatOverride = formatter 36 | } 37 | renderer.setDefaultToolTipGenerator { dataset, series, item -> 38 | "${DATE_FORMAT.format(dataset.getXValue(series, item))} - ${formatter.format(dataset.getYValue(series, item))}" 39 | } 40 | isDomainGridlinesVisible = false 41 | isRangeGridlinesVisible = false 42 | isOutlineVisible = false 43 | } 44 | 45 | padding = RectangleInsets(10.0, 10.0, 10.0, 10.0) 46 | isBorderVisible = false 47 | 48 | Kindling.theme.apply(this) 49 | Kindling.addThemeChangeListener { theme -> 50 | theme.apply(this) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/internal/DetailsIcon.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.internal 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import org.jdesktop.swingx.JXTable 5 | import org.jdesktop.swingx.decorator.HighlighterFactory 6 | import java.awt.event.MouseAdapter 7 | import java.awt.event.MouseEvent 8 | import javax.swing.JLabel 9 | import javax.swing.Popup 10 | import javax.swing.PopupFactory 11 | 12 | class DetailsIcon(details: Map) : JLabel(detailsIcon) { 13 | private val table = JXTable(DetailsModel(details.entries.toList())).apply { 14 | addHighlighter(HighlighterFactory.createSimpleStriping()) 15 | packAll() 16 | } 17 | 18 | init { 19 | alignmentY = 0.7F 20 | 21 | addMouseListener( 22 | object : MouseAdapter() { 23 | var popup: Popup? = null 24 | 25 | override fun mouseEntered(e: MouseEvent) { 26 | popup = PopupFactory.getSharedInstance().getPopup( 27 | this@DetailsIcon, 28 | table, 29 | locationOnScreen.x + detailsIcon.iconWidth, 30 | locationOnScreen.y, 31 | ).also { 32 | it.show() 33 | } 34 | } 35 | 36 | override fun mouseExited(e: MouseEvent) { 37 | popup?.hide() 38 | } 39 | }, 40 | ) 41 | } 42 | 43 | companion object { 44 | private val detailsIcon = FlatSVGIcon("icons/bx-search.svg").derive(0.75F) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/internal/DetailsModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.internal 2 | 3 | import io.github.paulgriffith.kindling.utils.ColumnList 4 | import javax.swing.table.AbstractTableModel 5 | import kotlin.properties.Delegates 6 | 7 | class DetailsModel(details: List>) : AbstractTableModel() { 8 | var details: List> by Delegates.observable(details) { _, _, _ -> 9 | fireTableDataChanged() 10 | } 11 | 12 | override fun getColumnName(column: Int): String = DetailsColumns[column].header 13 | override fun getRowCount(): Int = details.size 14 | override fun getColumnCount(): Int = size 15 | 16 | override fun getValueAt(row: Int, column: Int): Any? { 17 | return details[row].let { entry -> 18 | DetailsColumns[column].getValue(entry) 19 | } 20 | } 21 | 22 | override fun getColumnClass(column: Int): Class<*> = DetailsColumns[column].clazz 23 | 24 | @Suppress("unused") 25 | companion object DetailsColumns : ColumnList>() { 26 | val Key by column { it.key } 27 | val Value by column { it.value } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/internal/FileTransferHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.internal 2 | 3 | import java.awt.datatransfer.DataFlavor 4 | import java.awt.datatransfer.UnsupportedFlavorException 5 | import java.io.File 6 | import java.io.IOException 7 | import javax.swing.TransferHandler 8 | 9 | class FileTransferHandler(private val callback: (List) -> Unit) : TransferHandler() { 10 | override fun canImport(support: TransferSupport): Boolean { 11 | return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor) 12 | } 13 | 14 | @Suppress("UNCHECKED_CAST") 15 | override fun importData(support: TransferSupport): Boolean { 16 | if (!canImport(support)) { 17 | return false 18 | } 19 | val t = support.transferable 20 | try { 21 | val files = t.getTransferData(DataFlavor.javaFileListFlavor) as List 22 | callback.invoke(files) 23 | } catch (e: UnsupportedFlavorException) { 24 | return false 25 | } catch (e: IOException) { 26 | return false 27 | } 28 | return true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/log/Header.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.log 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import com.jidesoft.swing.JideButton 5 | import com.jidesoft.swing.JidePopupMenu 6 | import net.miginfocom.swing.MigLayout 7 | import org.jdesktop.swingx.JXSearchField 8 | import java.awt.event.MouseAdapter 9 | import java.awt.event.MouseEvent 10 | import java.time.ZoneId 11 | import java.time.zone.ZoneRulesProvider 12 | import javax.swing.ButtonGroup 13 | import javax.swing.JCheckBoxMenuItem 14 | import javax.swing.JLabel 15 | import javax.swing.JMenu 16 | import javax.swing.JPanel 17 | import kotlin.properties.Delegates 18 | 19 | class Header(private val totalRows: Int) : JPanel(MigLayout("ins 0, fill")) { 20 | private val events = JLabel("$totalRows (of $totalRows) events") 21 | 22 | val search = JXSearchField("Search") 23 | 24 | var isShowFullLoggerName: Boolean by Delegates.observable(false) { property, oldValue, newValue -> 25 | firePropertyChange(property.name, oldValue, newValue) 26 | } 27 | 28 | var selectedTimeZone: String by Delegates.observable(ZoneId.systemDefault().id) { property, oldValue, newValue -> 29 | firePropertyChange(property.name, oldValue, newValue) 30 | } 31 | 32 | var minimumLevel: Level by Delegates.observable(Level.INFO) { property, oldValue, newValue -> 33 | firePropertyChange(property.name, oldValue, newValue) 34 | } 35 | 36 | private val settingsMenu = JidePopupMenu().apply { 37 | add( 38 | JCheckBoxMenuItem("Show Full Logger Names").apply { 39 | addActionListener { 40 | isShowFullLoggerName = !isShowFullLoggerName 41 | } 42 | }, 43 | ) 44 | 45 | val tzGroup = ButtonGroup() 46 | add( 47 | JMenu("Timezone").apply { 48 | for (timezone in ZoneRulesProvider.getAvailableZoneIds().sorted()) { 49 | add(JCheckBoxMenuItem(timezone, timezone == selectedTimeZone)).also { timezoneItem -> 50 | tzGroup.add(timezoneItem) 51 | timezoneItem.addActionListener { 52 | selectedTimeZone = timezone 53 | } 54 | } 55 | } 56 | }, 57 | ) 58 | 59 | val levelGroup = ButtonGroup() 60 | add( 61 | JMenu("Minimum Level").apply { 62 | for (level in Level.values()) { 63 | add(JCheckBoxMenuItem(level.toString(), level == minimumLevel)).also { levelItem -> 64 | levelGroup.add(levelItem) 65 | levelItem.addActionListener { 66 | minimumLevel = level 67 | } 68 | } 69 | } 70 | }, 71 | ) 72 | } 73 | 74 | private val settings = JideButton(FlatSVGIcon("icons/bx-cog.svg")).apply { 75 | addMouseListener( 76 | object : MouseAdapter() { 77 | override fun mousePressed(e: MouseEvent) { 78 | settingsMenu.show(this@apply, e.x, e.y) 79 | } 80 | }, 81 | ) 82 | } 83 | 84 | init { 85 | add(events, "pushx") 86 | add(search, "width 300, gap unrelated") 87 | add(settings) 88 | } 89 | 90 | var displayedRows by Delegates.observable(totalRows) { _, _, newValue -> 91 | events.text = "$newValue (of $totalRows) events" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/log/LoggerNames.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.log 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import com.jidesoft.swing.CheckBoxList 5 | import io.github.paulgriffith.kindling.utils.Action 6 | import io.github.paulgriffith.kindling.utils.FlatScrollPane 7 | import io.github.paulgriffith.kindling.utils.NoSelectionModel 8 | import io.github.paulgriffith.kindling.utils.installSearchable 9 | import io.github.paulgriffith.kindling.utils.listCellRenderer 10 | import net.miginfocom.swing.MigLayout 11 | import javax.swing.AbstractListModel 12 | import javax.swing.ButtonGroup 13 | import javax.swing.JPanel 14 | import javax.swing.JToggleButton 15 | import javax.swing.ListModel 16 | 17 | data class LoggerName( 18 | val name: String, 19 | val eventCount: Int, 20 | ) 21 | 22 | class LoggerNamesModel(val data: List) : AbstractListModel() { 23 | override fun getSize(): Int = data.size + 1 24 | override fun getElementAt(index: Int): Any { 25 | return if (index == 0) { 26 | CheckBoxList.ALL_ENTRY 27 | } else { 28 | data[index - 1] 29 | } 30 | } 31 | } 32 | 33 | class LoggerNamesList(model: LoggerNamesModel) : CheckBoxList(model) { 34 | var isShowFullLoggerName = false 35 | set(value) { 36 | field = value 37 | repaint() 38 | } 39 | 40 | private fun displayValue(value: Any?): String { 41 | return when (value) { 42 | is LoggerName -> if (!isShowFullLoggerName) { 43 | value.name.substringAfterLast('.') 44 | } else { 45 | value.name 46 | } 47 | 48 | else -> value.toString() 49 | } 50 | } 51 | 52 | init { 53 | installSearchable( 54 | setup = { 55 | isCaseSensitive = false 56 | isRepeats = true 57 | isCountMatch = true 58 | }, 59 | conversion = ::displayValue, 60 | ) 61 | selectionModel = NoSelectionModel() 62 | cellRenderer = listCellRenderer { _, value, _, _, _ -> 63 | when (value) { 64 | is LoggerName -> { 65 | text = "${displayValue(value)} - [${value.eventCount}]" 66 | toolTipText = value.name 67 | } 68 | 69 | else -> { 70 | text = value.toString() 71 | } 72 | } 73 | } 74 | 75 | selectAll() 76 | } 77 | 78 | override fun getModel(): LoggerNamesModel = super.getModel() as LoggerNamesModel 79 | 80 | override fun setModel(model: ListModel<*>) { 81 | require(model is LoggerNamesModel) 82 | val selection = checkBoxListSelectedValues 83 | checkBoxListSelectionModel.valueIsAdjusting = true 84 | super.setModel(model) 85 | addCheckBoxListSelectedValues(selection) 86 | checkBoxListSelectionModel.valueIsAdjusting = false 87 | } 88 | } 89 | 90 | class LoggerNamesPanel(events: List) : JPanel(MigLayout("ins 0, fill")) { 91 | val list: LoggerNamesList = run { 92 | val loggerNames: List = events.groupingBy { it.logger } 93 | .eachCount() 94 | .entries 95 | .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.key }) 96 | .map { (key, value) -> LoggerName(key, value) } 97 | LoggerNamesList(LoggerNamesModel(loggerNames)) 98 | } 99 | 100 | init { 101 | val sortButtons = ButtonGroup() 102 | 103 | fun sortButton(icon: FlatSVGIcon, tooltip: String, comparator: Comparator): JToggleButton { 104 | return JToggleButton( 105 | Action( 106 | description = tooltip, 107 | icon = icon, 108 | ) { 109 | list.model = LoggerNamesModel(list.model.data.sortedWith(comparator)) 110 | }, 111 | ) 112 | } 113 | 114 | val naturalAsc = sortButton( 115 | icon = NATURAL_SORT_ASCENDING, 116 | tooltip = "Sort A-Z", 117 | comparator = byName, 118 | ) 119 | listOf( 120 | naturalAsc, 121 | sortButton( 122 | icon = NATURAL_SORT_DESCENDING, 123 | tooltip = "Sort Z-A", 124 | comparator = byName.reversed(), 125 | ), 126 | sortButton( 127 | icon = NUMERIC_SORT_DESCENDING, 128 | tooltip = "Sort by Count", 129 | comparator = byCount.reversed() then byName, 130 | ), 131 | sortButton( 132 | icon = NUMERIC_SORT_ASCENDING, 133 | tooltip = "Sort by Count (ascending)", 134 | comparator = byCount then byName, 135 | ), 136 | ).forEach { sortButton -> 137 | sortButtons.add(sortButton) 138 | add(sortButton, "cell 0 0") 139 | } 140 | 141 | sortButtons.setSelected(naturalAsc.model, true) 142 | 143 | add(FlatScrollPane(list), "newline, push, grow") 144 | } 145 | 146 | companion object { 147 | private val byName = compareBy(String.CASE_INSENSITIVE_ORDER, LoggerName::name) 148 | private val byCount = compareBy(LoggerName::eventCount) 149 | 150 | private val NATURAL_SORT_ASCENDING = FlatSVGIcon("icons/bx-sort-a-z.svg") 151 | private val NATURAL_SORT_DESCENDING = FlatSVGIcon("icons/bx-sort-z-a.svg") 152 | private val NUMERIC_SORT_ASCENDING = FlatSVGIcon("icons/bx-sort-up.svg") 153 | private val NUMERIC_SORT_DESCENDING = FlatSVGIcon("icons/bx-sort-down.svg") 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/log/SystemLogsEvent.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.log 2 | 3 | import java.time.Instant 4 | 5 | sealed interface LogEvent { 6 | val timestamp: Instant 7 | val message: String 8 | val logger: String 9 | } 10 | 11 | data class WrapperLogEvent( 12 | override val timestamp: Instant, 13 | override val message: String, 14 | override val logger: String = STDOUT, 15 | val level: Level? = null, 16 | val stacktrace: List = emptyList(), 17 | ) : LogEvent { 18 | companion object { 19 | const val STDOUT = "STDOUT" 20 | } 21 | } 22 | 23 | data class SystemLogsEvent( 24 | override val timestamp: Instant, 25 | override val message: String, 26 | override val logger: String, 27 | val thread: String, 28 | val level: Level, 29 | val mdc: Map, 30 | val stacktrace: List, 31 | ) : LogEvent 32 | 33 | @Suppress("ktlint:trailing-comma-on-declaration-site") 34 | enum class Level { 35 | TRACE, 36 | DEBUG, 37 | INFO, 38 | WARN, 39 | ERROR; 40 | 41 | companion object { 42 | private val firstChars = values().associateBy { it.name.first() } 43 | 44 | fun valueOf(char: Char): Level = firstChars.getValue(char) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/log/WrapperLogView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.log 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.paulgriffith.kindling.core.ClipboardTool 5 | import io.github.paulgriffith.kindling.core.MultiTool 6 | import io.github.paulgriffith.kindling.core.ToolPanel 7 | import io.github.paulgriffith.kindling.utils.Action 8 | import java.awt.Desktop 9 | import java.io.File 10 | import java.nio.file.Path 11 | import java.time.LocalTime 12 | import java.time.format.DateTimeFormatter 13 | import javax.swing.Icon 14 | import javax.swing.JPopupMenu 15 | import kotlin.io.path.name 16 | import kotlin.io.path.useLines 17 | 18 | class WrapperLogView( 19 | events: List, 20 | tabName: String, 21 | private val fromFile: Boolean, 22 | ) : ToolPanel() { 23 | private val logPanel = LogPanel(events) 24 | 25 | init { 26 | name = tabName 27 | toolTipText = tabName 28 | 29 | add(logPanel, "push, grow") 30 | } 31 | 32 | override val icon: Icon = LogViewer.icon 33 | 34 | override fun customizePopupMenu(menu: JPopupMenu) { 35 | menu.add( 36 | exportMenu { logPanel.table.model }, 37 | ) 38 | if (fromFile) { 39 | menu.addSeparator() 40 | menu.add( 41 | Action(name = "Open in External Editor") { 42 | Desktop.getDesktop().open(File(tabName)) 43 | }, 44 | ) 45 | } 46 | } 47 | } 48 | 49 | object LogViewer : MultiTool, ClipboardTool { 50 | override val title = "Wrapper Log" 51 | override val description = "wrapper.log(.n) files" 52 | override val icon = FlatSVGIcon("icons/bx-file.svg") 53 | override val extensions = listOf("log", "1", "2", "3", "4", "5") 54 | 55 | override fun open(paths: List): ToolPanel { 56 | require(paths.isNotEmpty()) { "Must provide at least one path" } 57 | val events = paths.flatMap { path -> 58 | path.useLines { lines -> LogPanel.parseLogs(lines) } 59 | } 60 | return WrapperLogView( 61 | events = events, 62 | tabName = paths.first().name, 63 | fromFile = true, 64 | ) 65 | } 66 | 67 | override fun open(data: String): ToolPanel { 68 | return WrapperLogView( 69 | events = LogPanel.parseLogs(data.lineSequence()), 70 | tabName = "Paste at ${LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))}", 71 | fromFile = false, 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/log/models.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.log // ktlint-disable filename 2 | 3 | import com.jidesoft.comparator.AlphanumComparator 4 | import io.github.paulgriffith.kindling.utils.Column 5 | import io.github.paulgriffith.kindling.utils.ColumnList 6 | import io.github.paulgriffith.kindling.utils.ReifiedLabelProvider 7 | import org.jdesktop.swingx.renderer.DefaultTableRenderer 8 | import java.time.Instant 9 | import javax.swing.table.AbstractTableModel 10 | 11 | class LogsModel( 12 | val data: List, 13 | val columns: ColumnList, 14 | ) : AbstractTableModel() { 15 | override fun getColumnName(column: Int): String = columns[column].header 16 | override fun getRowCount(): Int = data.size 17 | override fun getColumnCount(): Int = columns.size 18 | override fun getValueAt(row: Int, column: Int): Any? = get(row, columns[column]) 19 | override fun getColumnClass(column: Int): Class<*> = columns[column].clazz 20 | 21 | operator fun get(row: Int): T = data[row] 22 | operator fun get(row: Int, column: Column): R? { 23 | return data.getOrNull(row)?.let { event -> 24 | column.getValue(event) 25 | } 26 | } 27 | } 28 | 29 | @Suppress("unused", "PropertyName") 30 | class SystemLogsColumns(panel: LogPanel) : ColumnList() { 31 | val Level by column( 32 | column = { 33 | minWidth = 55 34 | maxWidth = 55 35 | }, 36 | value = { it.level }, 37 | ) 38 | val Timestamp by column( 39 | column = { 40 | minWidth = 155 41 | maxWidth = 155 42 | cellRenderer = DefaultTableRenderer { 43 | panel.dateFormatter.format(it as Instant) 44 | } 45 | }, 46 | value = SystemLogsEvent::timestamp, 47 | ) 48 | val Thread by column( 49 | column = { 50 | minWidth = 50 51 | }, 52 | value = { it.thread }, 53 | ) 54 | val Logger by column( 55 | column = { 56 | minWidth = 50 57 | 58 | val valueExtractor: (String?) -> String? = { 59 | if (panel.header.isShowFullLoggerName) { 60 | it 61 | } else { 62 | it?.substringAfterLast('.') 63 | } 64 | } 65 | 66 | cellRenderer = DefaultTableRenderer( 67 | ReifiedLabelProvider( 68 | getText = valueExtractor, 69 | getTooltip = { it }, 70 | ), 71 | ) 72 | comparator = compareBy(AlphanumComparator(), valueExtractor) 73 | }, 74 | value = SystemLogsEvent::logger, 75 | ) 76 | 77 | val Message by column { it.message } 78 | } 79 | 80 | @Suppress("unused", "PropertyName") 81 | class WrapperLogColumns(panel: LogPanel) : ColumnList() { 82 | val Level by column( 83 | column = { 84 | minWidth = 55 85 | maxWidth = 55 86 | }, 87 | value = { it.level }, 88 | ) 89 | val Timestamp by column( 90 | column = { 91 | minWidth = 155 92 | maxWidth = 155 93 | cellRenderer = DefaultTableRenderer { 94 | panel.dateFormatter.format(it as Instant) 95 | } 96 | }, 97 | value = { it.timestamp }, 98 | ) 99 | val Logger by column( 100 | column = { 101 | minWidth = 50 102 | 103 | val valueExtractor: (String?) -> String? = { 104 | if (panel.header.isShowFullLoggerName) { 105 | it 106 | } else { 107 | it?.substringAfterLast('.') 108 | } 109 | } 110 | 111 | cellRenderer = DefaultTableRenderer( 112 | ReifiedLabelProvider( 113 | getText = valueExtractor, 114 | getTooltip = { it }, 115 | ), 116 | ) 117 | comparator = compareBy(AlphanumComparator(), valueExtractor) 118 | }, 119 | value = { it.logger }, 120 | ) 121 | val Message by column { it.message } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/thread/FilterList.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.thread 2 | 3 | import com.jidesoft.swing.CheckBoxList 4 | import com.jidesoft.swing.ListSearchable 5 | import io.github.paulgriffith.kindling.utils.NoSelectionModel 6 | import io.github.paulgriffith.kindling.utils.listCellRenderer 7 | import java.text.DecimalFormat 8 | import javax.swing.AbstractListModel 9 | import javax.swing.ListModel 10 | 11 | typealias FilterComparator = Comparator> 12 | 13 | class FilterModel(val rawData: Map) : AbstractListModel() { 14 | var comparator: FilterComparator = byCountDesc 15 | set(value) { 16 | values = rawData.entries.sortedWith(value).map { it.key } 17 | fireContentsChanged(this, 0, size) 18 | field = value 19 | } 20 | 21 | private var values = rawData.entries.sortedWith(comparator).map { it.key } 22 | 23 | override fun getSize(): Int = values.size + 1 24 | override fun getElementAt(index: Int): Any? { 25 | return if (index == 0) { 26 | CheckBoxList.ALL_ENTRY 27 | } else { 28 | values[index - 1] 29 | } 30 | } 31 | 32 | fun indexOf(value: String): Int { 33 | val indexOf = values.indexOf(value) 34 | return if (indexOf >= 0) { 35 | indexOf + 1 36 | } else { 37 | -1 38 | } 39 | } 40 | 41 | companion object { 42 | val byNameAsc: FilterComparator = compareBy(nullsFirst(String.CASE_INSENSITIVE_ORDER)) { it.key } 43 | val byNameDesc: FilterComparator = byNameAsc.reversed() 44 | val byCountAsc: FilterComparator = compareBy { it.value } 45 | val byCountDesc: FilterComparator = byCountAsc.reversed() 46 | } 47 | } 48 | 49 | class FilterList(private val emptyLabel: String) : CheckBoxList(FilterModel(emptyMap())) { 50 | private var total = 0 51 | private var percentages = emptyMap() 52 | 53 | private var lastSelection = arrayOf() 54 | 55 | init { 56 | selectionModel = NoSelectionModel() 57 | isClickInCheckBoxOnly = false 58 | 59 | cellRenderer = listCellRenderer { _, value, _, _, _ -> 60 | text = when (value) { 61 | is String -> "$value - ${model.rawData[value]} (${percentages.getValue(value)})" 62 | null -> "$emptyLabel - ${model.rawData[null]} (${percentages.getValue(null)})" 63 | else -> value.toString() 64 | } 65 | } 66 | 67 | object : ListSearchable(this) { 68 | init { 69 | isCaseSensitive = false 70 | isRepeats = true 71 | isCountMatch = true 72 | } 73 | 74 | override fun convertElementToString(element: Any?): String = element.toString() 75 | 76 | override fun setSelectedIndex(index: Int, incremental: Boolean) { 77 | checkBoxListSelectedIndex = index 78 | } 79 | } 80 | } 81 | 82 | fun select(value: String) { 83 | val rowToSelect = model.indexOf(value) 84 | checkBoxListSelectionModel.setSelectionInterval(rowToSelect, rowToSelect) 85 | } 86 | 87 | override fun getModel(): FilterModel = super.getModel() as FilterModel 88 | 89 | override fun setModel(model: ListModel<*>) { 90 | require(model is FilterModel) 91 | val currentSelection = checkBoxListSelectedValues 92 | lastSelection = if (currentSelection.isEmpty()) { 93 | lastSelection 94 | } else { 95 | currentSelection 96 | } 97 | 98 | try { 99 | checkBoxListSelectionModel.valueIsAdjusting = true 100 | 101 | super.setModel(model) 102 | 103 | total = model.rawData.values.sum() 104 | percentages = model.rawData.mapValues { (_, count) -> 105 | val percentage = count.toFloat() / total 106 | DecimalFormat.getPercentInstance().format(percentage) 107 | } 108 | addCheckBoxListSelectedValues(lastSelection) 109 | } finally { 110 | checkBoxListSelectionModel.valueIsAdjusting = false 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/thread/ThreadDumpCheckboxList.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.thread 2 | 3 | import com.jidesoft.swing.CheckBoxList 4 | import io.github.paulgriffith.kindling.utils.NoSelectionModel 5 | import io.github.paulgriffith.kindling.utils.listCellRenderer 6 | import java.nio.file.Path 7 | import javax.swing.AbstractListModel 8 | import javax.swing.JList 9 | import javax.swing.ListModel 10 | import kotlin.io.path.name 11 | 12 | class ThreadDumpListModel(private val values: List) : AbstractListModel() { 13 | override fun getSize(): Int = values.size + 1 14 | override fun getElementAt(index: Int): Any? = when (index) { 15 | 0 -> CheckBoxList.ALL_ENTRY 16 | else -> values[index - 1] 17 | } 18 | } 19 | 20 | class ThreadDumpCheckboxList(data: List) : CheckBoxList(ThreadDumpListModel(data)) { 21 | init { 22 | layoutOrientation = JList.HORIZONTAL_WRAP 23 | visibleRowCount = 0 24 | isClickInCheckBoxOnly = false 25 | selectionModel = NoSelectionModel() 26 | 27 | cellRenderer = listCellRenderer { _, value, index, _, _ -> 28 | text = when (index) { 29 | 0 -> "All" 30 | else -> index.toString() 31 | } 32 | toolTipText = when (value) { 33 | is Path -> value.name 34 | else -> null 35 | } 36 | } 37 | selectAll() 38 | } 39 | 40 | override fun getModel() = super.getModel() as ThreadDumpListModel 41 | 42 | override fun setModel(model: ListModel<*>) { 43 | require(model is ThreadDumpListModel) 44 | val selection = checkBoxListSelectedValues 45 | checkBoxListSelectionModel.valueIsAdjusting = true 46 | super.setModel(model) 47 | addCheckBoxListSelectedValues(selection) 48 | checkBoxListSelectionModel.valueIsAdjusting = false 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/thread/model/NoneAsNullStringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.thread.model 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.builtins.serializer 5 | import kotlinx.serialization.descriptors.SerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | 9 | object NoneAsNullStringSerializer : KSerializer { 10 | override val descriptor: SerialDescriptor = String.serializer().descriptor 11 | 12 | override fun deserialize(decoder: Decoder): String? { 13 | return decoder.decodeString().takeIf { it != "None" } 14 | } 15 | 16 | override fun serialize(encoder: Encoder, value: String?) { 17 | when (value) { 18 | "None" -> Unit 19 | null -> Unit 20 | else -> encoder.encodeString(value) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/thread/model/Thread.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.thread.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import java.lang.Thread.State 6 | 7 | typealias Stacktrace = List 8 | 9 | @Serializable 10 | data class Thread( 11 | val id: Int, 12 | val name: String, 13 | val state: State, 14 | @SerialName("daemon") 15 | val isDaemon: Boolean, 16 | @Serializable(with = NoneAsNullStringSerializer::class) 17 | val system: String? = null, 18 | val scope: String? = null, 19 | val cpuUsage: Double? = null, 20 | val lockedMonitors: List = emptyList(), 21 | val lockedSynchronizers: List = emptyList(), 22 | @SerialName("waitingFor") 23 | val blocker: Blocker? = null, 24 | val stacktrace: Stacktrace = emptyList(), 25 | ) { 26 | var marked: Boolean = false 27 | 28 | val pool: String? = extractPool(name) 29 | 30 | @Serializable 31 | data class Monitors( 32 | val lock: String, 33 | val frame: String? = null, 34 | ) 35 | 36 | @Serializable 37 | data class Blocker( 38 | val lock: String, 39 | val owner: Int? = null, 40 | ) { 41 | override fun toString(): String = if (owner != null) { 42 | "$lock (owned by $owner)" 43 | } else { 44 | lock 45 | } 46 | } 47 | 48 | companion object { 49 | private val threadPoolRegex = "(?.+)-\\d+\$".toRegex() 50 | 51 | internal fun extractPool(name: String): String? { 52 | return threadPoolRegex.find(name)?.groups?.get("pool")?.value 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/thread/model/ThreadDump.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.thread.model 2 | 3 | import io.github.paulgriffith.kindling.core.ToolOpeningException 4 | import io.github.paulgriffith.kindling.utils.getLogger 5 | import io.github.paulgriffith.kindling.utils.getValue 6 | import kotlinx.serialization.SerialName 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.SerializationException 9 | import kotlinx.serialization.json.Json 10 | import java.io.InputStream 11 | import java.lang.Thread.State as ThreadState 12 | 13 | @Serializable 14 | data class ThreadDump internal constructor( 15 | val version: String, 16 | val threads: List, 17 | @SerialName("deadlocks") 18 | val deadlockIds: List = emptyList(), 19 | ) { 20 | companion object { 21 | private val JSON = Json { 22 | ignoreUnknownKeys = true 23 | } 24 | 25 | private val logger = getLogger() 26 | 27 | fun fromStream(stream: InputStream): ThreadDump? { 28 | val text = stream.reader().readText() 29 | return try { 30 | JSON.decodeFromString(serializer(), text) 31 | } catch (ex: SerializationException) { 32 | val lines = text.lines() 33 | if (lines.size <= 2) throw ToolOpeningException("Not a fully formed thread dump") 34 | val firstLine = lines.first() 35 | 36 | val deadlockIds = if (lines[2].contains("Deadlock")) { 37 | deadlocksPattern.findAll(lines[3]).map { match -> match.value.toInt() }.toList() 38 | } else { 39 | emptyList() 40 | } 41 | 42 | ThreadDump( 43 | version = versionPattern.find(firstLine)?.value 44 | ?: throw ToolOpeningException("No version, not a thread dump"), 45 | threads = when { 46 | firstLine.contains(":") -> parseScript(text) 47 | else -> parseWebPage(text) 48 | }, 49 | deadlockIds = deadlockIds, 50 | ) 51 | } 52 | } 53 | 54 | private val versionPattern = """[78]\.\d\.\d\d?.*""".toRegex() 55 | 56 | private val deadlocksPattern = """\d+""".toRegex() 57 | 58 | private val scriptThreadRegex = """ 59 | "(?.*)" 60 | \s*CPU:\s(?\d{1,3}\.\d{2})% 61 | \s*java\.lang\.Thread\.State:\s(?\w+_?\w+) 62 | \s*(?[\S\s]+?)[\r\n]{2,} 63 | """.trimIndent().toRegex(RegexOption.COMMENTS) 64 | 65 | private val webThreadRegex = """ 66 | (?Daemon )?Thread \[(?.*)] id=(?\d*), \((?\w*)\)\s*(?:\(native\))?\s* 67 | ?(?\s{4}[\S\s]+?)? 68 | ?(?=(?:Daemon )?Thread |") 69 | """.trimIndent().toRegex() 70 | private val webThreadMonitorRegex = "owns monitor: (?.*)".toRegex() 71 | private val webThreadSynchronizerRegex = "owns synchronizer: (?.*)".toRegex() 72 | private val webThreadBlockerRegex = "waiting for: (?\\S+)(?: \\(owned by (?\\d*))?".toRegex() 73 | private val webThreadStackRegex = "^(?(?!waiting |owns ).*)$".toRegex(RegexOption.MULTILINE) 74 | 75 | private fun parseScript(dump: String): List { 76 | return scriptThreadRegex.findAll(dump).map { matcher -> 77 | val name by matcher.groups 78 | val cpu by matcher.groups 79 | val state by matcher.groups 80 | val stack by matcher.groups 81 | 82 | Thread( 83 | id = name.value.hashCode(), 84 | name = name.value, 85 | cpuUsage = cpu.value.toDouble(), 86 | state = ThreadState.valueOf(state.value), 87 | isDaemon = false, 88 | stacktrace = stack.value.lines().map(String::trim), 89 | ) 90 | }.toList() 91 | } 92 | 93 | private fun parseWebPage(dump: String): List { 94 | return webThreadRegex.findAll(dump).map { matcher -> 95 | val isDaemon = matcher.groups["isDaemon"]?.value != null 96 | val name by matcher.groups 97 | val id by matcher.groups 98 | val state by matcher.groups 99 | val stack = matcher.groups["stack"]?.value?.trimIndent() ?: "" 100 | val monitors = webThreadMonitorRegex.findAll(stack).mapNotNull { monitorMatcher -> 101 | monitorMatcher.groups["monitor"]?.value?.let { 102 | Thread.Monitors(it) 103 | } 104 | }.toList() 105 | val synchronizers = webThreadSynchronizerRegex.findAll(stack).mapNotNull { synchronizerMatcher -> 106 | synchronizerMatcher.groups["synchronizer"]?.value 107 | }.toList() 108 | val blocker = webThreadBlockerRegex.find(stack)?.groups?.let { blockerMatcher -> 109 | Thread.Blocker(blockerMatcher["lock"]!!.value, blockerMatcher["owner"]?.value?.toIntOrNull()) 110 | } 111 | val parsedStack = webThreadStackRegex.findAll(stack).mapNotNull { stackMatcher -> 112 | stackMatcher.groups["line"]?.value 113 | }.toList() 114 | 115 | Thread( 116 | id = id.value.toInt(), 117 | name = name.value, 118 | state = ThreadState.valueOf(state.value), 119 | isDaemon = isDaemon, 120 | blocker = blocker, 121 | lockedMonitors = monitors, 122 | lockedSynchronizers = synchronizers, 123 | stacktrace = parsedStack, 124 | ) 125 | }.toList() 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/utils/Action.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.utils 2 | 3 | import java.awt.event.ActionEvent 4 | import java.awt.event.ActionListener 5 | import javax.swing.AbstractAction 6 | import javax.swing.Icon 7 | import javax.swing.KeyStroke 8 | import kotlin.properties.ReadWriteProperty 9 | import kotlin.reflect.KProperty 10 | 11 | /** 12 | * More idiomatic Kotlin wrapper for AbstractAction. 13 | */ 14 | class Action( 15 | name: String? = null, 16 | description: String? = null, 17 | icon: Icon? = null, 18 | accelerator: KeyStroke? = null, 19 | private val action: ActionListener, 20 | ) : AbstractAction() { 21 | var name: String? by actionValue(NAME, name) 22 | var description: String? by actionValue(SHORT_DESCRIPTION, description) 23 | var icon: Icon? by actionValue(SMALL_ICON, icon) 24 | var accelerator: KeyStroke? by actionValue(ACCELERATOR_KEY, accelerator) 25 | 26 | private fun actionValue(name: String, initialValue: V) = object : ReadWriteProperty { 27 | init { 28 | putValue(name, initialValue) 29 | } 30 | 31 | @Suppress("UNCHECKED_CAST") 32 | override fun getValue(thisRef: AbstractAction, property: KProperty<*>): V { 33 | return thisRef.getValue(name) as V 34 | } 35 | 36 | override fun setValue(thisRef: AbstractAction, property: KProperty<*>, value: V) { 37 | return thisRef.putValue(name, value) 38 | } 39 | } 40 | 41 | override fun actionPerformed(e: ActionEvent) = action.actionPerformed(e) 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/utils/Column.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.utils 2 | 3 | import org.jdesktop.swingx.table.TableColumnExt 4 | import javax.swing.table.TableModel 5 | 6 | data class Column( 7 | val header: String, 8 | val getValue: (row: R) -> C, 9 | val columnCustomization: (TableColumnExt.(model: TableModel) -> Unit)?, 10 | val clazz: Class, 11 | ) { 12 | companion object { 13 | inline operator fun invoke( 14 | header: String, 15 | noinline columnCustomization: (TableColumnExt.(model: TableModel) -> Unit)? = null, 16 | noinline getValue: (row: R) -> C, 17 | ) = Column( 18 | header = header, 19 | columnCustomization = columnCustomization, 20 | getValue = getValue, 21 | clazz = C::class.java, 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/utils/ColumnList.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.utils 2 | 3 | import org.jdesktop.swingx.table.ColumnFactory 4 | import org.jdesktop.swingx.table.TableColumnExt 5 | import javax.swing.table.TableModel 6 | import kotlin.properties.PropertyDelegateProvider 7 | import kotlin.properties.ReadOnlyProperty 8 | 9 | abstract class ColumnList private constructor( 10 | @PublishedApi internal val list: MutableList>, 11 | ) : List> by list { 12 | constructor() : this(mutableListOf()) 13 | 14 | /** 15 | * Defines a new column (type T). Uses the name of the property if [name] isn't provided. 16 | */ 17 | // This is some real Kotlin 'magic', but makes it very easy to define JTable models that can be used type-safely 18 | protected inline fun column( 19 | name: String? = null, 20 | noinline column: (TableColumnExt.(model: TableModel) -> Unit)? = null, 21 | noinline value: (R) -> T, 22 | ): PropertyDelegateProvider, ReadOnlyProperty, Column>> { 23 | return PropertyDelegateProvider { thisRef, prop -> 24 | val actual = Column( 25 | header = name ?: prop.name, 26 | getValue = value, 27 | columnCustomization = column, 28 | clazz = T::class.java, 29 | ) 30 | thisRef.add(actual) 31 | ReadOnlyProperty { _, _ -> actual } 32 | } 33 | } 34 | 35 | fun add(column: Column) { 36 | list.add(column) 37 | } 38 | 39 | operator fun get(column: Column): Int = indexOf(column) 40 | 41 | fun toColumnFactory() = object : ColumnFactory() { 42 | override fun configureTableColumn(model: TableModel, columnExt: TableColumnExt) { 43 | super.configureTableColumn(model, columnExt) 44 | val column = list[columnExt.modelIndex] 45 | columnExt.toolTipText = column.header 46 | column.columnCustomization?.invoke(columnExt, model) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/utils/ScrollingTextPane.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.utils 2 | 3 | import com.formdev.flatlaf.extras.components.FlatScrollPane 4 | import java.awt.Dimension 5 | import java.awt.Rectangle 6 | import javax.swing.JTextPane 7 | import javax.swing.event.HyperlinkListener 8 | 9 | class ScrollingTextPane : FlatScrollPane() { 10 | var text: String? 11 | get() = textPane.text 12 | set(value) { 13 | val lineHeight = 21 14 | val lineCount = value?.lineSequence()?.count() ?: 1 15 | val newPrefHeight = ((lineHeight * lineCount) + 2 * horizontalScrollBar.preferredSize.height).coerceAtMost(250) 16 | preferredSize = Dimension(Integer.MAX_VALUE, newPrefHeight) 17 | textPane.text = value 18 | viewport.scrollRectToVisible(Rectangle(0, 0)) 19 | } 20 | 21 | private val textPane = JTextPane().apply { 22 | isEditable = false 23 | contentType = "text/html" 24 | } 25 | 26 | fun addHyperlinkListener(listener: HyperlinkListener) { 27 | textPane.addHyperlinkListener(listener) 28 | } 29 | 30 | init { 31 | setViewportView(textPane) 32 | 33 | horizontalScrollBar.preferredSize = Dimension(0, SCROLLBAR_WIDTH) 34 | verticalScrollBar.preferredSize = Dimension(SCROLLBAR_WIDTH, 0) 35 | 36 | verticalScrollBarPolicy = VERTICAL_SCROLLBAR_ALWAYS 37 | preferredSize = Dimension(Integer.MAX_VALUE, 0) 38 | viewport.scrollRectToVisible(Rectangle(0, 0)) 39 | } 40 | 41 | companion object { 42 | private const val SCROLLBAR_WIDTH = 10 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/utils/TabStrip.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.utils 2 | 3 | import com.formdev.flatlaf.extras.components.FlatTabbedPane 4 | import java.awt.BorderLayout 5 | import java.awt.Component 6 | import java.awt.Container 7 | import java.awt.event.ComponentAdapter 8 | import java.awt.event.ComponentEvent 9 | import javax.swing.Icon 10 | import javax.swing.JComponent 11 | import javax.swing.JFrame 12 | import javax.swing.JMenu 13 | import javax.swing.JMenuBar 14 | import javax.swing.JPanel 15 | import javax.swing.JPopupMenu 16 | 17 | interface PopupMenuCustomizer { 18 | fun customizePopupMenu(menu: JPopupMenu) 19 | } 20 | 21 | interface FloatableComponent { 22 | val icon: Icon? 23 | val tabName: String 24 | val tabTooltip: String 25 | } 26 | 27 | class TabStrip : FlatTabbedPane() { 28 | init { 29 | tabPlacement = TOP 30 | tabLayoutPolicy = SCROLL_TAB_LAYOUT 31 | tabAlignment = TabAlignment.leading 32 | isTabsClosable = true 33 | 34 | setTabCloseCallback { _, i -> 35 | removeTabAt(i) 36 | } 37 | 38 | attachPopupMenu { event -> 39 | val tabIndex = indexAtLocation(event.x, event.y) 40 | if (tabIndex == -1) return@attachPopupMenu null 41 | 42 | val tab = getComponentAt(tabIndex) as JComponent 43 | 44 | JPopupMenu().apply { 45 | add( 46 | Action("Close") { 47 | removeClosableTabAt(tabIndex) 48 | }, 49 | ) 50 | add( 51 | Action("Close Other Tabs") { 52 | for (i in tabCount - 1 downTo 0) { 53 | if (i != tabIndex) { 54 | removeClosableTabAt(i) 55 | } 56 | } 57 | }, 58 | ) 59 | add( 60 | Action("Close Tabs Left") { 61 | for (i in tabIndex - 1 downTo 0) { 62 | removeClosableTabAt(i) 63 | } 64 | }, 65 | ) 66 | add( 67 | Action("Close Tabs Right") { 68 | for (i in tabCount - 1 downTo tabIndex + 1) { 69 | removeClosableTabAt(i) 70 | } 71 | }, 72 | ) 73 | val closable = isTabClosable(tabIndex) 74 | add( 75 | Action(if (closable) "Pin" else "Unpin") { 76 | setTabClosable(tabIndex, !closable) 77 | }, 78 | ) 79 | if (tab is FloatableComponent) { 80 | add( 81 | Action("Float") { 82 | val frame = createPopupFrame(tab) 83 | frame.isVisible = true 84 | }, 85 | ) 86 | } 87 | if (tab is PopupMenuCustomizer) { 88 | tab.customizePopupMenu(this) 89 | } 90 | } 91 | } 92 | } 93 | 94 | private fun removeClosableTabAt(index: Int) { 95 | if (isTabClosable(index)) { 96 | removeTabAt(index) 97 | } 98 | } 99 | 100 | val indices: IntRange 101 | get() = 0 until tabCount 102 | 103 | fun addTab( 104 | component: T, 105 | tabName: String = component.tabName, 106 | tabTooltip: String? = component.tabTooltip, 107 | icon: Icon? = component.icon, 108 | select: Boolean = true, 109 | ) where T : Container, T : FloatableComponent { 110 | addTab(tabName, icon, component, tabTooltip) 111 | if (select) { 112 | selectedIndex = indices.last 113 | } 114 | } 115 | 116 | fun addLazyTab( 117 | tabName: String, 118 | tabTooltip: String? = null, 119 | icon: Icon? = null, 120 | component: () -> T, 121 | ) where T : Container, T : FloatableComponent { 122 | addTab( 123 | tabName, 124 | icon, 125 | LazyTab(component), 126 | tabTooltip, 127 | ) 128 | } 129 | 130 | private class LazyTab(supplier: () -> Component) : JPanel(BorderLayout()) { 131 | private var initialized = false 132 | 133 | init { 134 | addComponentListener( 135 | object : ComponentAdapter() { 136 | override fun componentShown(e: ComponentEvent) { 137 | if (!initialized) { 138 | add(supplier(), BorderLayout.CENTER) 139 | initialized = true 140 | } 141 | } 142 | }, 143 | ) 144 | } 145 | } 146 | 147 | private fun createPopupFrame(tab: T): JFrame where T : Container, T : FloatableComponent { 148 | return jFrame(tab.tabName, 1024, 768) { 149 | contentPane = tab 150 | 151 | jMenuBar = JMenuBar().apply { 152 | add( 153 | JMenu("Actions").apply { 154 | add( 155 | Action(name = "Unfloat") { 156 | addTab( 157 | tab.tabName, 158 | tab.icon, 159 | tab, 160 | tab.tabTooltip, 161 | ) 162 | dispose() 163 | }, 164 | ) 165 | }, 166 | ) 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/utils/ZipFileTree.kt: -------------------------------------------------------------------------------- 1 | 2 | package io.github.paulgriffith.kindling.utils 3 | 4 | import com.jidesoft.comparator.AlphanumComparator 5 | import java.nio.file.FileSystem 6 | import java.nio.file.Path 7 | import javax.swing.JTree 8 | import javax.swing.tree.DefaultTreeModel 9 | import javax.swing.tree.TreeModel 10 | import kotlin.io.path.ExperimentalPathApi 11 | import kotlin.io.path.PathWalkOption.INCLUDE_DIRECTORIES 12 | import kotlin.io.path.div 13 | import kotlin.io.path.isDirectory 14 | import kotlin.io.path.name 15 | import kotlin.io.path.walk 16 | 17 | data class PathNode(override val userObject: Path) : TypedTreeNode() 18 | 19 | @OptIn(ExperimentalPathApi::class) 20 | class RootNode(zipFile: FileSystem) : AbstractTreeNode() { 21 | init { 22 | val pathComparator = compareBy { it.isDirectory() }.thenBy(AlphanumComparator()) { it.name } 23 | val zipFilePaths = zipFile.rootDirectories.asSequence() 24 | .flatMap { it.walk(INCLUDE_DIRECTORIES) } 25 | .sortedWith(pathComparator) 26 | 27 | val seen = mutableMapOf() 28 | for (path in zipFilePaths) { 29 | var lastSeen: AbstractTreeNode = this 30 | var currentDepth = zipFile.getPath("/") 31 | for (part in path) { 32 | currentDepth /= part 33 | val next = seen.getOrPut(currentDepth) { 34 | val newChild = PathNode(currentDepth) 35 | lastSeen.children.add(newChild) 36 | newChild 37 | } 38 | lastSeen = next 39 | } 40 | } 41 | } 42 | } 43 | 44 | class ZipFileModel(fileSystem: FileSystem) : DefaultTreeModel(RootNode(fileSystem)) 45 | 46 | class ZipFileTree(fileSystem: FileSystem) : JTree(ZipFileModel(fileSystem)) { 47 | init { 48 | isRootVisible = false 49 | setShowsRootHandles(true) 50 | 51 | setCellRenderer( 52 | treeCellRenderer { _, value, _, _, _, _, _ -> 53 | if (value is PathNode) { 54 | val path = value.userObject 55 | toolTipText = path.toString() 56 | text = path.last().toString() 57 | } 58 | this 59 | }, 60 | ) 61 | } 62 | 63 | override fun getModel(): ZipFileModel? = super.getModel() as ZipFileModel? 64 | override fun setModel(newModel: TreeModel?) { 65 | newModel as ZipFileModel 66 | super.setModel(newModel) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/utils/utils.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.utils // ktlint-disable filename 2 | 3 | import io.github.evanrupert.excelkt.workbook 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory 6 | import org.sqlite.SQLiteDataSource 7 | import java.io.File 8 | import java.io.InputStream 9 | import java.math.BigDecimal 10 | import java.nio.file.Path 11 | import java.sql.Connection 12 | import java.sql.Date 13 | import java.sql.JDBCType 14 | import java.sql.ResultSet 15 | import java.sql.Time 16 | import java.sql.Timestamp 17 | import java.util.Properties 18 | import java.util.ServiceLoader 19 | import javax.swing.table.TableModel 20 | import kotlin.math.log2 21 | import kotlin.math.pow 22 | import kotlin.reflect.KProperty 23 | 24 | fun String.truncate(length: Int = 20): String { 25 | return asIterable().joinToString(separator = "", limit = length) 26 | } 27 | 28 | inline fun getLogger(): Logger { 29 | return LoggerFactory.getLogger(T::class.java.name) 30 | } 31 | 32 | /** 33 | * Exhausts (and closes) a ResultSet into a list using [transform]. 34 | */ 35 | fun ResultSet.toList( 36 | transform: (ResultSet) -> T, 37 | ): List { 38 | return use { rs -> 39 | buildList { 40 | while (rs.next()) { 41 | add(transform(rs)) 42 | } 43 | } 44 | } 45 | } 46 | 47 | inline fun StringBuilder.tag(tag: String, content: StringBuilder.() -> Unit) { 48 | append("<").append(tag).append(">") 49 | content(this) 50 | append("") 51 | } 52 | 53 | fun StringBuilder.tag(tag: String, content: String) { 54 | tag(tag) { append(content) } 55 | } 56 | 57 | /** 58 | * Returns the mode (most common value) in a Grouping 59 | */ 60 | fun Grouping.mode(): Int? = eachCount().maxOfOrNull { it.key } 61 | 62 | val JDBCType.javaType: Class<*> 63 | get() = when (this) { 64 | JDBCType.BIT -> Boolean::class 65 | JDBCType.TINYINT -> Short::class 66 | JDBCType.SMALLINT -> Short::class 67 | JDBCType.INTEGER -> Int::class 68 | JDBCType.BIGINT -> Long::class 69 | JDBCType.FLOAT -> Float::class 70 | JDBCType.REAL -> Double::class 71 | JDBCType.DOUBLE -> Double::class 72 | JDBCType.NUMERIC -> BigDecimal::class 73 | JDBCType.DECIMAL -> BigDecimal::class 74 | JDBCType.CHAR -> String::class 75 | JDBCType.VARCHAR -> String::class 76 | JDBCType.LONGVARCHAR -> String::class 77 | JDBCType.DATE -> Date::class 78 | JDBCType.TIME -> Time::class 79 | JDBCType.TIMESTAMP -> Timestamp::class 80 | JDBCType.BINARY -> ByteArray::class 81 | JDBCType.VARBINARY -> ByteArray::class 82 | JDBCType.LONGVARBINARY -> ByteArray::class 83 | JDBCType.BOOLEAN -> Boolean::class 84 | JDBCType.ROWID -> Long::class 85 | JDBCType.NCHAR -> String::class 86 | JDBCType.NVARCHAR -> String::class 87 | JDBCType.LONGNVARCHAR -> String::class 88 | JDBCType.BLOB -> ByteArray::class 89 | JDBCType.CLOB -> ByteArray::class 90 | JDBCType.NCLOB -> ByteArray::class 91 | else -> Any::class 92 | }.javaObjectType 93 | 94 | fun SQLiteConnection(path: Path, readOnly: Boolean = true): Connection { 95 | return SQLiteDataSource().apply { 96 | url = "jdbc:sqlite:file:$path" 97 | setReadOnly(readOnly) 98 | }.connection 99 | } 100 | 101 | fun Properties(inputStream: InputStream): Properties = Properties().apply { load(inputStream) } 102 | 103 | private val prefix = arrayOf("", "k", "m", "g", "t", "p", "e", "z", "y") 104 | 105 | fun Long.toFileSizeLabel(): String = when { 106 | this == 0L -> "0B" 107 | else -> { 108 | val digits = log2(toDouble()).toInt() / 10 109 | val precision = digits.coerceIn(0, 2) 110 | "%,.${precision}f${prefix[digits]}b".format(toDouble() / 2.0.pow(digits * 10.0)) 111 | } 112 | } 113 | 114 | operator fun MatchGroupCollection.getValue(thisRef: Any?, property: KProperty<*>): MatchGroup { 115 | return requireNotNull(get(property.name)) 116 | } 117 | 118 | val TableModel.rowIndices get() = 0 until rowCount 119 | val TableModel.columnIndices get() = 0 until columnCount 120 | 121 | fun TableModel.exportToCSV(file: File) { 122 | file.printWriter().use { out -> 123 | columnIndices.joinTo(buffer = out, separator = ",") { col -> 124 | getColumnName(col) 125 | } 126 | out.println() 127 | for (row in rowIndices) { 128 | columnIndices.joinTo(buffer = out, separator = ",") { col -> 129 | "\"${getValueAt(row, col)?.toString().orEmpty()}\"" 130 | } 131 | out.println() 132 | } 133 | } 134 | } 135 | 136 | fun TableModel.exportToXLSX(file: File) = file.outputStream().use { fos -> 137 | workbook { 138 | sheet("Sheet 1") { // TODO: Some way to pipe in a more useful sheet name (or multiple sheets?) 139 | row { 140 | for (col in columnIndices) { 141 | cell(getColumnName(col)) 142 | } 143 | } 144 | for (row in rowIndices) { 145 | row { 146 | for (col in columnIndices) { 147 | when (val value = getValueAt(row, col)) { 148 | is Double -> cell( 149 | value, 150 | createCellStyle { 151 | dataFormat = xssfWorkbook.createDataFormat().getFormat("0.00") 152 | }, 153 | ) 154 | 155 | else -> cell(value ?: "") 156 | } 157 | } 158 | } 159 | } 160 | } 161 | }.xssfWorkbook.write(fos) 162 | } 163 | 164 | inline fun loadService(): ServiceLoader { 165 | return ServiceLoader.load(S::class.java) 166 | } 167 | 168 | fun String.escapeHtml(): String { 169 | return buildString { 170 | for (char in this@escapeHtml) { 171 | when (char) { 172 | '>' -> append(">") 173 | '<' -> append("<") 174 | else -> append(char) 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/zip/views/GenericFileView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.zip.views 2 | 3 | import io.github.paulgriffith.kindling.utils.FlatScrollPane 4 | import java.awt.EventQueue 5 | import java.awt.Font 6 | import java.awt.Rectangle 7 | import java.nio.file.Path 8 | import java.nio.file.spi.FileSystemProvider 9 | import java.util.HexFormat 10 | import javax.swing.Icon 11 | import javax.swing.JTextArea 12 | 13 | class GenericFileView(override val provider: FileSystemProvider, override val path: Path) : SinglePathView() { 14 | override val icon: Icon? = null 15 | 16 | private val textArea = JTextArea().apply { 17 | font = Font(Font.MONOSPACED, Font.PLAIN, 12) 18 | } 19 | 20 | init { 21 | provider.newInputStream(path).use { file -> 22 | val windowSize = 16 23 | textArea.text = sequence { 24 | val buffer = ByteArray(windowSize) 25 | var numberOfBytesRead: Int 26 | do { 27 | numberOfBytesRead = file.readNBytes(buffer, 0, windowSize) 28 | 29 | // the last read might not be complete, so there could be stale data in the buffer 30 | val toRead = buffer.sliceArray(0 until numberOfBytesRead) 31 | val hexBytes = HEX_FORMAT.formatHex(toRead) 32 | val decodedBytes = decodeBytes(toRead) 33 | yield("${hexBytes.padEnd(47)} $decodedBytes") 34 | } while (numberOfBytesRead == windowSize) 35 | }.joinToString(separator = "\n") 36 | } 37 | 38 | add(FlatScrollPane(textArea), "push, grow") 39 | EventQueue.invokeLater { 40 | textArea.scrollRectToVisible(Rectangle(0, 0)) 41 | } 42 | } 43 | 44 | private fun decodeBytes(toRead: ByteArray): String { 45 | return String( 46 | CharArray(toRead.size) { i -> 47 | val byte = toRead[i] 48 | if (byte >= 0 && !Character.isISOControl(byte.toInt())) { 49 | Char(byte.toUShort()) 50 | } else { 51 | '.' 52 | } 53 | }, 54 | ) 55 | } 56 | 57 | companion object { 58 | private val HEX_FORMAT: HexFormat = HexFormat.of().withDelimiter(" ") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/zip/views/ImageView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.zip.views 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import java.nio.file.Path 5 | import java.nio.file.spi.FileSystemProvider 6 | import javax.imageio.ImageIO 7 | import javax.swing.ImageIcon 8 | import javax.swing.JLabel 9 | import javax.swing.SwingConstants.CENTER 10 | import kotlin.io.path.extension 11 | 12 | class ImageView(override val provider: FileSystemProvider, override val path: Path) : SinglePathView() { 13 | init { 14 | val imageInputStream = ImageIO.createImageInputStream(provider.newInputStream(path)) 15 | val image = imageInputStream.use { iis -> 16 | val reader = ImageIO.getImageReaders(iis).next() 17 | reader.input = iis 18 | reader.read(0) 19 | } 20 | 21 | add( 22 | JLabel().apply { 23 | horizontalAlignment = CENTER 24 | verticalAlignment = CENTER 25 | icon = ImageIcon(image) 26 | }, 27 | "center", 28 | ) 29 | } 30 | 31 | override val icon: FlatSVGIcon = FlatSVGIcon("icons/bx-image.svg").derive(16, 16) 32 | 33 | companion object { 34 | private val KNOWN_EXTENSIONS = setOf( 35 | "png", 36 | "bmp", 37 | "gif", 38 | "jpg", 39 | "jpeg", 40 | ) 41 | 42 | fun isImageFile(path: Path) = path.extension in KNOWN_EXTENSIONS 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/zip/views/MultiToolView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.zip.views 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.paulgriffith.kindling.core.MultiTool 5 | import io.github.paulgriffith.kindling.core.Tool 6 | import io.github.paulgriffith.kindling.core.ToolPanel 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | import java.nio.file.spi.FileSystemProvider 10 | import javax.swing.JPopupMenu 11 | import kotlin.io.path.extension 12 | import kotlin.io.path.name 13 | import kotlin.io.path.nameWithoutExtension 14 | import kotlin.io.path.outputStream 15 | 16 | class MultiToolView( 17 | override val provider: FileSystemProvider, 18 | override val paths: List, 19 | ) : PathView("ins 0, fill") { 20 | private val multiTool: MultiTool 21 | private val toolPanel: ToolPanel 22 | 23 | override val tabName by lazy { 24 | val roots = paths.mapTo(mutableSetOf()) { path -> 25 | path.nameWithoutExtension.trimEnd { it.isDigit() || it == '-' || it == '.' } 26 | } 27 | "[${paths.size}] ${roots.joinToString()}.${paths.first().extension}" 28 | } 29 | override val tabTooltip by lazy { paths.joinToString("\n") { it.toString().substring(1) } } 30 | 31 | override fun toString(): String = "MultiToolView(paths=$paths)" 32 | 33 | init { 34 | val tempFiles = paths.map { path -> 35 | Files.createTempFile("kindling", path.name).also { tempFile -> 36 | provider.newInputStream(path).use { file -> 37 | tempFile.outputStream().use(file::copyTo) 38 | } 39 | } 40 | } 41 | 42 | multiTool = Tool[tempFiles.first().toFile()] as MultiTool 43 | toolPanel = multiTool.open(tempFiles) 44 | 45 | add(toolPanel, "push, grow") 46 | } 47 | 48 | override val icon: FlatSVGIcon = toolPanel.icon as FlatSVGIcon 49 | 50 | override fun customizePopupMenu(menu: JPopupMenu) = toolPanel.customizePopupMenu(menu) 51 | } 52 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/zip/views/PathView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.zip.views 2 | 3 | import io.github.paulgriffith.kindling.utils.FloatableComponent 4 | import io.github.paulgriffith.kindling.utils.PopupMenuCustomizer 5 | import net.miginfocom.swing.MigLayout 6 | import java.nio.file.Path 7 | import java.nio.file.spi.FileSystemProvider 8 | import javax.swing.JPanel 9 | import javax.swing.JPopupMenu 10 | import kotlin.io.path.name 11 | 12 | sealed class PathView(constraints: String) : JPanel(MigLayout(constraints)), FloatableComponent, PopupMenuCustomizer { 13 | abstract val paths: List 14 | abstract val provider: FileSystemProvider 15 | override fun customizePopupMenu(menu: JPopupMenu) = Unit 16 | } 17 | 18 | abstract class SinglePathView(constraints: String = "ins 6, fill") : PathView(constraints) { 19 | protected abstract val path: Path 20 | 21 | override val paths: List by lazy { listOf(path) } 22 | override val tabName by lazy { path.name } 23 | override val tabTooltip by lazy { path.toString().substring(1) } 24 | override fun toString(): String = "${this::class.simpleName}(path=$path)" 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/zip/views/ProjectView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.zip.views 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.paulgriffith.kindling.core.Kindling 5 | import java.nio.file.FileVisitResult 6 | import java.nio.file.Path 7 | import java.nio.file.spi.FileSystemProvider 8 | import java.util.zip.ZipEntry 9 | import java.util.zip.ZipOutputStream 10 | import javax.swing.JButton 11 | import javax.swing.JFileChooser 12 | import javax.swing.filechooser.FileNameExtensionFilter 13 | import kotlin.io.path.ExperimentalPathApi 14 | import kotlin.io.path.div 15 | import kotlin.io.path.name 16 | import kotlin.io.path.outputStream 17 | import kotlin.io.path.readBytes 18 | import kotlin.io.path.visitFileTree 19 | 20 | @OptIn(ExperimentalPathApi::class) 21 | class ProjectView(override val provider: FileSystemProvider, override val path: Path) : SinglePathView() { 22 | private val exportButton = JButton("Export Project") 23 | 24 | init { 25 | exportButton.addActionListener { 26 | exportZipFileChooser.selectedFile = Kindling.homeLocation.resolve("${path.name}.zip") 27 | if (exportZipFileChooser.showSaveDialog(this@ProjectView) == JFileChooser.APPROVE_OPTION) { 28 | val exportLocation = exportZipFileChooser.selectedFile.toPath() 29 | 30 | ZipOutputStream(exportLocation.outputStream()).use { zos -> 31 | path.visitFileTree { 32 | onVisitFile { file, _ -> 33 | zos.run { 34 | putNextEntry(ZipEntry(path.relativize(file).toString())) 35 | write(file.readBytes()) 36 | closeEntry() 37 | FileVisitResult.CONTINUE 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | add(exportButton, "north") 46 | add(TextFileView(provider, path / "project.json"), "push, grow") 47 | } 48 | 49 | override val icon: FlatSVGIcon = FlatSVGIcon("icons/bx-box.svg").derive(16, 16) 50 | 51 | companion object { 52 | val exportZipFileChooser = JFileChooser(Kindling.homeLocation).apply { 53 | isMultiSelectionEnabled = false 54 | isAcceptAllFileFilterUsed = false 55 | fileSelectionMode = JFileChooser.FILES_ONLY 56 | fileFilter = FileNameExtensionFilter("ZIP Files", "zip") 57 | 58 | Kindling.addThemeChangeListener { 59 | updateUI() 60 | } 61 | } 62 | 63 | fun isProjectDirectory(path: Path) = path.parent?.name == "projects" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/zip/views/TextFileView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.zip.views 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.paulgriffith.kindling.core.Kindling 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.json.Json 7 | import kotlinx.serialization.json.JsonElement 8 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea 9 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants.SYNTAX_STYLE_CSS 10 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants.SYNTAX_STYLE_JSON 11 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants.SYNTAX_STYLE_NONE 12 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE 13 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants.SYNTAX_STYLE_PYTHON 14 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants.SYNTAX_STYLE_XML 15 | import org.fife.ui.rtextarea.RTextScrollPane 16 | import java.awt.EventQueue 17 | import java.awt.Rectangle 18 | import java.nio.file.Path 19 | import java.nio.file.spi.FileSystemProvider 20 | import kotlin.io.path.extension 21 | import kotlin.io.path.name 22 | 23 | class TextFileView(override val provider: FileSystemProvider, override val path: Path) : SinglePathView() { 24 | private val textArea = RSyntaxTextArea().apply { 25 | isEditable = false 26 | syntaxEditingStyle = KNOWN_EXTENSIONS[path.extension] ?: SYNTAX_STYLE_NONE 27 | 28 | Kindling.theme.apply(this) 29 | } 30 | 31 | override val icon: FlatSVGIcon = FlatSVGIcon("icons/bx-file.svg").derive(16, 16) 32 | 33 | init { 34 | val text = provider.newInputStream(path).use { 35 | it.bufferedReader().readText() 36 | } 37 | 38 | textArea.text = if (path.extension == "json") { 39 | // pretty-print/normalize json 40 | JSON_FORMAT.run { 41 | encodeToString(JsonElement.serializer(), parseToJsonElement(text)) 42 | } 43 | } else { 44 | text 45 | } 46 | 47 | Kindling.addThemeChangeListener { theme -> 48 | theme.apply(textArea) 49 | } 50 | 51 | add(RTextScrollPane(textArea), "push, grow") 52 | EventQueue.invokeLater { 53 | textArea.scrollRectToVisible(Rectangle(0, 0)) 54 | } 55 | } 56 | 57 | companion object { 58 | @OptIn(ExperimentalSerializationApi::class) 59 | private val JSON_FORMAT = Json { 60 | prettyPrint = true 61 | prettyPrintIndent = " " 62 | } 63 | 64 | private val KNOWN_EXTENSIONS = mapOf( 65 | "conf" to SYNTAX_STYLE_PROPERTIES_FILE, 66 | "properties" to SYNTAX_STYLE_PROPERTIES_FILE, 67 | "py" to SYNTAX_STYLE_PYTHON, 68 | "json" to SYNTAX_STYLE_JSON, 69 | "svg" to SYNTAX_STYLE_XML, 70 | "xml" to SYNTAX_STYLE_XML, 71 | "css" to SYNTAX_STYLE_CSS, 72 | "txt" to SYNTAX_STYLE_NONE, 73 | "md" to SYNTAX_STYLE_NONE, 74 | "p7b" to SYNTAX_STYLE_NONE, 75 | "log" to SYNTAX_STYLE_NONE, 76 | ) 77 | 78 | private val KNOWN_FILENAMES = setOf( 79 | "README", 80 | ".uuid", 81 | "wrapper.log.1", 82 | "wrapper.log.2", 83 | "wrapper.log.3", 84 | "wrapper.log.4", 85 | "wrapper.log.5", 86 | ) 87 | 88 | fun isTextFile(path: Path) = path.extension in KNOWN_EXTENSIONS || path.name in KNOWN_FILENAMES 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/paulgriffith/kindling/zip/views/ToolView.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.zip.views 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.paulgriffith.kindling.core.Tool 5 | import io.github.paulgriffith.kindling.core.ToolOpeningException 6 | import io.github.paulgriffith.kindling.core.ToolPanel 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | import java.nio.file.spi.FileSystemProvider 10 | import java.util.zip.ZipException 11 | import javax.swing.JPopupMenu 12 | import kotlin.io.path.extension 13 | import kotlin.io.path.name 14 | import kotlin.io.path.outputStream 15 | 16 | class ToolView( 17 | override val provider: FileSystemProvider, 18 | override val path: Path, 19 | ) : SinglePathView("ins 0, fill") { 20 | private val toolPanel: ToolPanel 21 | 22 | init { 23 | val tempFile = Files.createTempFile("kindling", path.name) 24 | try { 25 | provider.newInputStream(path).use { file -> 26 | tempFile.outputStream().use(file::copyTo) 27 | } 28 | /* Tool.get() throws exception if tool not found, but this check is already done with isTool() */ 29 | toolPanel = Tool.byExtension[path.extension]?.open(tempFile) 30 | ?: throw ToolOpeningException("No tool for files of type .${path.extension}") 31 | add(toolPanel, "push, grow") 32 | } catch (e: ZipException) { 33 | throw ToolOpeningException("Unable to open $path .${path.extension}") 34 | } 35 | } 36 | 37 | override val icon: FlatSVGIcon = (toolPanel.icon as FlatSVGIcon).derive(16, 16) 38 | 39 | override fun customizePopupMenu(menu: JPopupMenu) = toolPanel.customizePopupMenu(menu) 40 | 41 | companion object { 42 | fun maybeIsTool(path: Path) = path.extension in Tool.byExtension 43 | 44 | fun safelyCreate(provider: FileSystemProvider, path: Path): ToolView? { 45 | return runCatching { ToolView(provider, path) }.getOrNull() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-chip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-cog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-column.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-data.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-detail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-file-find.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-hdd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-link-external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sort-a-z.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sort-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sort-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sort-z-a.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/ignition.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paul-griffith/kindling/bf963b2d7bab04414ed418059f94825e039d33ce/src/main/resources/icons/ignition.icns -------------------------------------------------------------------------------- /src/main/resources/icons/ignition.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paul-griffith/kindling/bf963b2d7bab04414ed418059f94825e039d33ce/src/main/resources/icons/ignition.ico -------------------------------------------------------------------------------- /src/main/resources/icons/ignition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paul-griffith/kindling/bf963b2d7bab04414ed418059f94825e039d33ce/src/main/resources/icons/ignition.png -------------------------------------------------------------------------------- /src/main/resources/icons/kindling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paul-griffith/kindling/bf963b2d7bab04414ed418059f94825e039d33ce/src/main/resources/icons/kindling.png -------------------------------------------------------------------------------- /src/main/resources/icons/null.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | NULL 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/kotlin/io/github/paulgriffith/kindling/thread/ThreadViewTests.kt: -------------------------------------------------------------------------------- 1 | package io.github.paulgriffith.kindling.thread 2 | 3 | import io.github.paulgriffith.kindling.thread.model.Thread.Companion.extractPool 4 | import io.github.paulgriffith.kindling.thread.model.ThreadDump 5 | import io.kotest.assertions.asClue 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.data.blocking.forAll 8 | import io.kotest.data.row 9 | import io.kotest.matchers.shouldBe 10 | 11 | class ThreadViewTests : FunSpec( 12 | { 13 | test("Thread JSON deserialization") { 14 | ThreadDump.fromStream(ThreadViewTests::class.java.getResourceAsStream("threadDump.json")!!)!! 15 | .asClue { (version, threads) -> 16 | version shouldBe "Dev" 17 | threads.size shouldBe 2 18 | } 19 | } 20 | test("Deadlock JSON deserialization") { 21 | ThreadDump.fromStream(ThreadViewTests::class.java.getResourceAsStream("deadlockThreadDump.json")!!)!! 22 | .asClue { (version, threads, deadlockIds) -> 23 | version shouldBe "8.1.16.2022040511" 24 | threads.size shouldBe 5 25 | deadlockIds.size shouldBe 3 26 | } 27 | } 28 | context("Legacy parsing") { 29 | test("From webpage") { 30 | ThreadDump.fromStream(ThreadViewTests::class.java.getResourceAsStream("legacyWebThreadDump.txt")!!)!! 31 | .asClue { (version, threads) -> 32 | version shouldBe "7.9.14 (b2020042813)" 33 | threads.size shouldBe 4 34 | } 35 | } 36 | test("From Auto-Generated Deadlock") { 37 | ThreadDump.fromStream(ThreadViewTests::class.java.getResourceAsStream("legacyDeadlockThreadDump.txt")!!)!! 38 | .asClue { (version, threads, deadlockIds) -> 39 | version shouldBe "8.1.7 (b2021060314)" 40 | threads.size shouldBe 5 41 | deadlockIds.size shouldBe 3 42 | } 43 | } 44 | test("From scripting") { 45 | ThreadDump.fromStream(ThreadViewTests::class.java.getResourceAsStream("legacyScriptThreadDump.txt")!!)!! 46 | .asClue { (version, threads) -> 47 | version shouldBe "8.1.1 (b2020120808)" 48 | threads.size shouldBe 3 49 | } 50 | } 51 | } 52 | 53 | test("Thread Pool Parsing Tests") { 54 | forAll( 55 | row("gateway-logging-sqlite-appender", null), 56 | row("gateway-performance-metric-history-1", "gateway-performance-metric-history"), 57 | row("gateway-performance-metric-history-2", "gateway-performance-metric-history"), 58 | row("gateway-shared-exec-engine-11", "gateway-shared-exec-engine"), 59 | row("gateway-storeforward-pipeline[postgres]-engine[PrimarySFEngine]", null), 60 | row("gateway.tags.subscriptionmodel-1", "gateway.tags.subscriptionmodel"), 61 | row("HSQLDB Timer @32c91059", null), 62 | row("HttpClient-1-SelectorManager", null), 63 | row("HttpClient@25d4330e-1129", "HttpClient@25d4330e"), 64 | row("HttpClient@25d4330e-1315", "HttpClient@25d4330e"), 65 | row("milo-netty-event-loop-0", "milo-netty-event-loop"), 66 | row("opc-ua-executor-18", "opc-ua-executor"), 67 | row("opc-ua-executor-19", "opc-ua-executor"), 68 | row("Session-Scheduler-782a4fff-1", "Session-Scheduler-782a4fff"), 69 | row("webserver-1114", "webserver"), 70 | row( 71 | // maybe someday 72 | "webserver-43-acceptor-0@25cd7918-ServerConnector@1d7f7be7{SSL, (ssl, http/1.1)}{0.0.0.0:8060}", 73 | null, 74 | ), 75 | row("AsyncSocketIOSession[I/O]-1", "AsyncSocketIOSession[I/O]"), 76 | ) { name, pool -> 77 | extractPool(name) shouldBe pool 78 | } 79 | } 80 | }, 81 | ) 82 | -------------------------------------------------------------------------------- /src/test/resources/io/github/paulgriffith/kindling/thread/legacyScriptThreadDump.txt: -------------------------------------------------------------------------------- 1 | Ignition version: 8.1.1 (b2020120808) 2 | 3 | "AsyncAppender-Worker-DBAsync" 4 | CPU: 0.00% 5 | java.lang.Thread.State: WAITING 6 | at java.base@11.0.7/jdk.internal.misc.Unsafe.park(Native Method) 7 | at java.base@11.0.7/java.util.concurrent.locks.LockSupport.park(Unknown Source) 8 | at java.base@11.0.7/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(Unknown Source) 9 | at java.base@11.0.7/java.util.concurrent.ArrayBlockingQueue.take(Unknown Source) 10 | at app//ch.qos.logback.core.AsyncAppenderBase$Worker.run(AsyncAppenderBase.java:264) 11 | 12 | "AsyncAppender-Worker-SysoutAsync" 13 | CPU: 0.00% 14 | java.lang.Thread.State: WAITING 15 | at java.base@11.0.7/jdk.internal.misc.Unsafe.park(Native Method) 16 | at java.base@11.0.7/java.util.concurrent.locks.LockSupport.park(Unknown Source) 17 | at java.base@11.0.7/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(Unknown Source) 18 | at java.base@11.0.7/java.util.concurrent.ArrayBlockingQueue.take(Unknown Source) 19 | at app//ch.qos.logback.core.AsyncAppenderBase$Worker.run(AsyncAppenderBase.java:264) 20 | 21 | "AsyncSocketIOSession[I/O]-1" 22 | CPU: 0.27% 23 | java.lang.Thread.State: RUNNABLE 24 | at java.base@11.0.7/java.net.SocketInputStream.socketRead0(Native Method) 25 | at java.base@11.0.7/java.net.SocketInputStream.socketRead(Unknown Source) 26 | at java.base@11.0.7/java.net.SocketInputStream.read(Unknown Source) 27 | at java.base@11.0.7/java.net.SocketInputStream.read(Unknown Source) 28 | at java.base@11.0.7/java.net.SocketInputStream.read(Unknown Source) 29 | at com.inductiveautomation.iosession.socket.AsyncSocketIOSession.run(AsyncSocketIOSession.java:71) 30 | at java.base@11.0.7/java.lang.Thread.run(Unknown Source) 31 | 32 | -------------------------------------------------------------------------------- /src/test/resources/io/github/paulgriffith/kindling/thread/legacyWebThreadDump.txt: -------------------------------------------------------------------------------- 1 | "Ignition v7.9.14 (b2020042813) 2 | 3 | Daemon Thread [AsyncAppender-Worker-DBAsync] id=30, (WAITING) 4 | waiting for: java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@334d8df8 5 | sun.misc.Unsafe.park(Native Method) 6 | java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) 7 | java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) 8 | java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403) 9 | ch.qos.logback.core.AsyncAppenderBase$Worker.run(AsyncAppenderBase.java:264) 10 | Thread [webserver-46] id=46, (RUNNABLE) 11 | sun.management.ThreadImpl.dumpThreads0(Native Method) 12 | sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:454) 13 | com.inductiveautomation.ignition.gateway.web.pages.status.routes.DiagnosticsRoutes$FormattedThreadDump.createDump(DiagnosticsRoutes.java:322) 14 | com.inductiveautomation.ignition.gateway.web.pages.status.routes.DiagnosticsRoutes.formattedDump(DiagnosticsRoutes.java:281) 15 | com.inductiveautomation.ignition.gateway.web.pages.status.routes.DiagnosticsRoutes$$Lambda$91/1349965380.handle(Unknown Source) 16 | com.inductiveautomation.ignition.gateway.dataroutes.Route.service(Route.java:211) 17 | com.inductiveautomation.ignition.gateway.dataroutes.RouteGroupImpl.service(RouteGroupImpl.java:49) 18 | com.inductiveautomation.ignition.gateway.dataroutes.DataServletServicerImpl.service(DataServletServicerImpl.java:78) 19 | com.inductiveautomation.ignition.gateway.bootstrap.DataServlet.service(DataServlet.java:25) 20 | javax.servlet.http.HttpServlet.service(HttpServlet.java:790) 21 | org.eclipse.jetty.servlet.ServletHolder$NotAsyncServlet.service(ServletHolder.java:1391) 22 | org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:760) 23 | org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:547) 24 | org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) 25 | org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:590) 26 | org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) 27 | org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235) 28 | org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1607) 29 | org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233) 30 | org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1297) 31 | org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188) 32 | org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:485) 33 | org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1577) 34 | org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186) 35 | org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1212) 36 | org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) 37 | org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:59) 38 | org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) 39 | org.eclipse.jetty.server.Server.handle(Server.java:500) 40 | org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383) 41 | org.eclipse.jetty.server.HttpChannel$$Lambda$375/864466841.dispatch(Unknown Source) 42 | org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:547) 43 | org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375) 44 | org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:270) 45 | org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311) 46 | org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103) 47 | org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117) 48 | org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336) 49 | org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313) 50 | org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171) 51 | org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129) 52 | org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:388) 53 | org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806) 54 | org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938) 55 | java.lang.Thread.run(Thread.java:748) 56 | Thread [webserver-47-acceptor-0@7dbd82ae-ServerConnector@3b504c0b{HTTP/1.1,[http/1.1]}{0.0.0.0:8088}] id=47, (RUNNABLE) (native) 57 | owns monitor: java.lang.Object@4742e437 58 | sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method) 59 | sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:419) 60 | sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:247) 61 | org.eclipse.jetty.server.ServerConnector.accept(ServerConnector.java:385) 62 | org.eclipse.jetty.server.AbstractConnector$Acceptor.run(AbstractConnector.java:701) 63 | org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806) 64 | org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938) 65 | java.lang.Thread.run(Thread.java:748) 66 | Thread [xopc-tag-driver-2] id=116, (WAITING) 67 | waiting for: java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@6f4e28da 68 | sun.misc.Unsafe.park(Native Method) 69 | java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) 70 | java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) 71 | java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1088) 72 | java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809) 73 | java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) 74 | java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) 75 | java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 76 | java.lang.Thread.run(Thread.java:748) 77 | " -------------------------------------------------------------------------------- /src/test/resources/io/github/paulgriffith/kindling/thread/threadDump.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "Dev", 3 | "threads": [ 4 | { 5 | "name": "HSQLDB Timer @4551b699", 6 | "id": 93, 7 | "state": "TIMED_WAITING", 8 | "daemon": true, 9 | "system": "None", 10 | "scope": "Gateway", 11 | "cpuUsage": 0, 12 | "waitingFor": { 13 | "lock": "org.hsqldb.lib.HsqlTimer$TaskQueue@135e1e67" 14 | }, 15 | "stacktrace": [ 16 | "java.base@11.0.11/java.lang.Object.wait(Native Method)", 17 | "app//org.hsqldb.lib.HsqlTimer$TaskQueue.park(Unknown Source)", 18 | "app//org.hsqldb.lib.HsqlTimer.nextTask(Unknown Source)", 19 | "app//org.hsqldb.lib.HsqlTimer$TaskRunner.run(Unknown Source)", 20 | "java.base@11.0.11/java.lang.Thread.run(Thread.java:829)" 21 | ] 22 | }, 23 | { 24 | "name": "HttpClient-1-SelectorManager", 25 | "id": 46, 26 | "state": "RUNNABLE", 27 | "daemon": true, 28 | "system": "None", 29 | "scope": "Gateway", 30 | "cpuUsage": 0, 31 | "lockedMonitors": [ 32 | { 33 | "lock": "sun.nio.ch.Util$2@6150057e", 34 | "frame": "java.base@11.0.11/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:124)" 35 | }, 36 | { 37 | "lock": "sun.nio.ch.WindowsSelectorImpl@48522463", 38 | "frame": "java.base@11.0.11/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:124)" 39 | } 40 | ], 41 | "stacktrace": [ 42 | "java.base@11.0.11/sun.nio.ch.WindowsSelectorImpl$SubSelector.poll0(Native Method)", 43 | "java.base@11.0.11/sun.nio.ch.WindowsSelectorImpl$SubSelector.poll(WindowsSelectorImpl.java:357)", 44 | "java.base@11.0.11/sun.nio.ch.WindowsSelectorImpl.doSelect(WindowsSelectorImpl.java:182)", 45 | "java.base@11.0.11/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:124)", 46 | "java.base@11.0.11/sun.nio.ch.SelectorImpl.select(SelectorImpl.java:136)", 47 | "platform/java.net.http@11.0.11/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:867)" 48 | ] 49 | } 50 | ] 51 | } --------------------------------------------------------------------------------