diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..85fde7d3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,79 @@ +name: Bug report +description: Report a reproducible problem with the plugin +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for opening an issue! 🙌 + + Please provide as much detail as possible so we can reproduce and investigate the problem. + + - type: textarea + id: description + attributes: + label: Bug description + description: What happened? + placeholder: Describe the problem clearly. + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Please provide a detailed, reproducible description. + placeholder: | + 1. Open ... + 2. Click ... + 3. See error ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + placeholder: What did you expect to happen? + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual behavior + placeholder: What actually happened? + validations: + required: true + + - type: textarea + id: stacktrace + attributes: + label: Stack trace or logs + description: Paste any stack trace or relevant logs here, if available. + render: text + + - type: textarea + id: screenshots + attributes: + label: Screenshots or screen recordings + description: Add screenshots or recordings if they help explain the issue. + + - type: input + id: ide-version + attributes: + label: JetBrains IDE and version + placeholder: IntelliJ IDEA 2024.3, WebStorm 2024.3, PyCharm 2024.3, etc. + + - type: input + id: plugin-version + attributes: + label: Plugin version + placeholder: 1.2.3 + + - type: textarea + id: additional-context + attributes: + label: Additional context + placeholder: Anything else that may help? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..cfcd4e7c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Questions or support + url: https://github.com/marcelkliemannel/intellij-developer-tools-plugin/discussions + about: Please use Discussions for questions, support requests, and general ideas. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..bff03954 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,55 @@ +name: Feature request +description: Suggest an improvement for the plugin +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for opening an issue! 🙌 + + Please note: this is a general IntelliJ plugin intended to work across all JetBrains IDEs. + + Features that are specific to one programming language, framework, or technology stack are usually out of scope for this plugin. + + - type: textarea + id: problem + attributes: + label: Problem description + description: What problem are you trying to solve? + placeholder: Describe the use case or pain point. + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: What would you like to happen? + placeholder: Describe the feature you would like to see. + validations: + required: true + + - type: dropdown + id: scope-confirmation + attributes: + label: Scope confirmation + description: Is this feature general enough to work across JetBrains IDEs? + options: + - Yes, this is a general feature for JetBrains IDEs + - No, this is specific to a language, framework, or technology stack + - Not sure + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + placeholder: Describe any alternatives or workarounds you have considered. + + - type: textarea + id: additional-context + attributes: + label: Additional context + placeholder: Add screenshots, examples, or other context here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/pull_request_template.md b/.github/ISSUE_TEMPLATE/pull_request_template.md new file mode 100644 index 00000000..2ea64f90 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/pull_request_template.md @@ -0,0 +1,33 @@ +## 🙌 Thanks for your contribution + +We really appreciate your effort in improving this plugin. + +--- + +## ⚠️ Before submitting a PR + +Please note: + +- It is **required to open an issue first** before submitting a pull request. +- The issue should clearly describe the problem and **propose a solution or approach**. +- This ensures that changes are aligned with the overall direction of the project. + +👉 **Unsolicited pull requests (without prior discussion) are usually not appropriate and may be closed.** + +--- + +## 🔗 Related Issue + +Please link the issue this PR is based on: + +Fixes #... + +--- + +## ✨ What does this PR do? + +Describe your changes clearly: +- What problem does it solve? +- What was your approach? + +--- diff --git a/.github/workflows/checkPlugin.yml b/.github/workflows/checkPlugin.yml index 12c29764..87c87087 100644 --- a/.github/workflows/checkPlugin.yml +++ b/.github/workflows/checkPlugin.yml @@ -4,10 +4,13 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + permissions: contents: read actions: read @@ -16,27 +19,31 @@ permissions: jobs: verifyPlugin: runs-on: ubuntu-latest + timeout-minutes: 60 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '21' + - uses: gradle/actions/setup-gradle@v6 + - name: Run Checks uses: coactions/setup-xvfb@v1 with: run: ./gradlew check --stacktrace + - name: Verify Plugin + run: ./gradlew verifyPlugin --stacktrace + - name: Test Report - uses: dorny/test-reporter@v1 - if: success() || failure() + uses: dorny/test-reporter@v3 + if: ${{ !cancelled() }} + continue-on-error: true with: name: JUnit Tests path: '**/build/test-results/test/TEST-*.xml' reporter: java-junit - - - name: Verify Plugin - run: ./gradlew verifyPlugin --stacktrace - diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..3bd4492d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,278 @@ +# AGENTS.md + +This repository is an IntelliJ Platform plugin written in Kotlin. It provides a "Developer Tools" tool window, standalone dialog, editor popup actions, and intention actions for common developer utilities. + +Use this file as the working map for future AI agents. It explains where the important code lives, how tools are registered, how state is persisted, and what commands to run before handing work back. + +## Project Shape + +- Root Gradle project: `intellij-developer-tools-plugin` +- Build system: Gradle Kotlin DSL with `org.jetbrains.intellij.platform` plugin. +- Language/runtime: Kotlin/JVM on Java 21. +- Minimum IDE build and platform are controlled from `gradle.properties`. +- The default `platform=idea` includes Java- and Kotlin-dependent modules. Non-IDEA platforms only include the platform/common modules. +- Plugin id is intentionally misspelled as `dev.turingcomplete.intellijdevelopertoolsplugins`; do not "fix" it. + +Important root files: + +- `build.gradle.kts`: root build, IntelliJ plugin packaging, signing, publishing, verification, common test/compiler config. +- `settings.gradle.kts`: module inclusion; conditionally includes `java-dependent` and `kotlin-dependent` when `platform == "idea"`. +- `gradle/libs.versions.toml`: dependency and plugin versions. +- `gradle.properties`: plugin metadata, platform version, bundled plugins, Kotlin stdlib opt-out. +- `src/main/resources/META-INF/plugin.xml`: main plugin descriptor and extension-point registry. +- `src/main/resources/META-INF/dev.turingcomplete.intellijdevelopertoolsplugins-withJava.xml`: optional Java plugin integrations. +- `src/main/resources/META-INF/dev.turingcomplete.intellijdevelopertoolsplugins-withKotlin.xml`: optional Kotlin plugin integrations. + +## Modules + +- `modules/common`: shared utilities, i18n bundle wrapper, plugin info, editor helpers, crypto/hash/text helpers, test fixtures. +- `modules/settings`: application and instance settings, settings UI abstractions, persistence and legacy migration for tool configurations. +- `modules/tools/editor`: editor popup actions and generic editor intentions for selected text. +- `modules/tools/ui`: the main UI tool framework, tool window/dialog implementation, all UI tools, shared Swing/UI DSL components, test fixtures. +- `modules/java-dependent`: Java PSI-specific editor actions and intentions. Only loaded through optional Java descriptor. +- `modules/kotlin-dependent`: Kotlin PSI-specific editor actions and intentions. Only loaded through optional Kotlin descriptor. +- `src/test`: root-level integration tests around plugin XML and cross-module tool behavior. + +## Plugin Entry Points + +The main descriptor is `src/main/resources/META-INF/plugin.xml`. + +It declares: + +- Required dependencies: platform, lang, json. +- Optional dependencies: `com.intellij.java` and `org.jetbrains.kotlin`, each with its own descriptor. +- Tool window: `MainToolWindowFactory`, id `Developer Tools`. +- Standalone dialog action: `OpenMainDialogAction`, added to `ToolsMenu`. +- Editor popup group: `DeveloperToolsActionGroup`, added to `EditorPopupMenu`. +- Main selected-text intention: `DataGeneratorIntentionAction`. +- Settings configurable: `GeneralSettingsConfigurable` with nested `JsonHandlingSettingsConfigurable`. +- Keymap extension: `ShowDeveloperUiToolKeymapExtension`. +- Custom extension points: + - `developerUiTool`: registers tool factories. + - `developerUiToolGroup`: registers menu groups. + - `developerToolConfigurationEnumPropertyType`: registers enum types that can be persisted in tool settings. + +The Java/Kotlin optional descriptors add language-specific intentions and editor action groups. Keep those registrations out of the main descriptor unless they do not depend on Java/Kotlin APIs. + +## UI Tool Framework + +Most new user-facing utilities should be implemented as a `DeveloperUiTool` in `modules/tools/ui`. + +Core classes: + +- `DeveloperUiTool`: base class for a tool UI. Subclasses implement `Panel.buildUi()`. It wraps UI DSL output, registers validation, supports activation/deactivation, reset, disposal, scroll wrapping, and optional `DataProvider` data. +- `DeveloperUiToolFactory`: creates tools and returns `DeveloperUiToolPresentation`. +- `DeveloperUiToolFactoryEp`: extension-point bean for `developerUiTool`; reads `id`, `implementationClass`, `groupId`, `preferredSelected`, and `internalTool` from `plugin.xml`. +- `DeveloperUiToolContext`: carries the extension id and whether vertical layout should be preferred, mainly for tool-window layout. +- `DeveloperUiToolPresentation`: titles/descriptions used in menu, grouped menu, and content panel. +- `DeveloperUiToolGroup`: extension-point bean for grouping tools in the menu tree. + +Registration flow: + +1. `plugin.xml` registers a ``. +2. `ToolsMenuTree` reads `DeveloperUiToolFactoryEp.EP_NAME`. +3. Each factory is instantiated and asked for a tool creator. +4. `DeveloperToolNode` restores or creates `DeveloperToolConfiguration` workbenches. +5. `DeveloperToolContentPanel` creates tabs, calls `DeveloperUiTool.createComponent()`, and drives `activated()`/`deactivated()`. + +If a factory returns `null` from `getDeveloperUiToolCreator`, the tool is hidden for that context. This is useful for project-dependent tools. + +## Dialog And Tool Window + +Two instances expose the same tool framework with different persistence scopes: + +- Dialog: + - `OpenMainDialogAction` opens `MainDialogService`. + - `MainDialog` uses `ContentPanelHandler` and `DeveloperToolsDialogSettings`. + - Dialog inputs/configuration are application-level. + +- Tool window: + - `MainToolWindowFactory` creates async tool-window content. + - `MainToolWindowService` stores and opens/selects tool-window content. + - `ToolWindowContentPanelHandler` uses `DeveloperToolsToolWindowSettings`. + - Tool-window inputs/configuration are project-level. + +`ContentPanelHandler` is the shared coordinator. It owns the menu tree, switches group/tool panels, caches panels depending on `generalSettings.toolWindowUiCacheUi`, and implements `openTool()`/`showTool()`. + +## Settings And Persistence + +Application settings: + +- `DeveloperToolsApplicationSettings` is an app service persisted to `developer-tools.xml`. +- It contains `GeneralSettings`, `InternalSettings`, and `JsonHandlingSettings`. +- Settings interfaces use annotations from `modules/settings/.../base`. + +Tool instance settings: + +- `DeveloperToolsInstanceSettings` is the base `PersistentStateComponent`. +- `DeveloperToolsDialogSettings` stores dialog state at app level. +- `DeveloperToolsToolWindowSettings` stores tool-window state at project level. +- Each UI tool workbench has a `DeveloperToolConfiguration`. +- Tool properties are registered by calling `configuration.register("key", defaultValue, propertyType, example)`. + +Persistence details to respect: + +- Only changed properties are persisted. +- `PropertyType.CONFIGURATION`, `INPUT`, and `SENSITIVE` are filtered by general settings. +- Sensitive values are not saved unless `saveSensitiveInputs` is enabled. +- Supported built-in persisted value types are in `DeveloperToolsInstanceSettings.builtInConfigurationPropertyTypes`. +- Enum configuration values must be registered in `plugin.xml` via ``. +- If renaming property keys or moving enum classes, update `DeveloperToolsInstanceSettingsLegacy` and the legacy test resources under `src/test/resources/.../instancesettings`. + +## Tool Implementation Patterns + +Common base classes in `modules/tools/ui/src/main/kotlin/.../tool/ui`: + +- `converter/base/Converter`: two-pane conversion base with source/target handlers, live conversion, validation, diff support, file/text handlers, and background conversion for heavy work. +- `converter/base/UndirectionalConverter`: one-way converter UI. +- `EncoderDecoder`: bidirectional converter base for encoding/decoding tools. +- `OneLineTextGenerator`: generator base for UUID/password/NanoID/etc.; handles generated value, copy, regenerate, and bulk generation. +- `MultiLineTextGenerator`: generator base for multi-line output. +- `AdvancedEditor`: shared editor component with input/output modes and diff support. +- `AsyncTaskExecutor`: debounced async UI/background task helper. +- `FileHandling`, `ErrorHolder`, validation helpers, regex helpers, and copy actions in `tool/ui/common`. + +Tool categories and representative files: + +- Converters: `tool/ui/converter`, including Base32/Base64, URL, ASCII, text format, date/time, CLI command, JWT, units. +- Transformers: `tool/ui/transformer`, including hashing/HMAC, text case, sorting/filtering, JSON Path, SQL and code formatting. +- Generators: `tool/ui/generator`, including UUID/ULID/NanoID/password/lorem/barcode. +- Other tools: `tool/ui/other`, including regex matcher, JSON schema validator, unarchiver, color picker, server certificates, HTTP server, notes, text diff/statistics, cron, ASCII art. + +When adding a new UI tool: + +1. Choose the closest base class instead of starting from `DeveloperUiTool` directly. +2. Put it in the matching package under `modules/tools/ui`. +3. Add a nested `Factory : DeveloperUiToolFactory`. +4. Register the factory in `plugin.xml` with a stable `id`. +5. Add enum property type registrations for any persisted enum defaults. +6. Use `configuration.register(...)` for all persisted controls. Pick stable keys. +7. Add bundle entries if the surrounding package uses message bundles. +8. Add or update tests when persistence, plugin XML registration, or conversion behavior changes. + +## Editor Actions And Intentions + +Generic selected-text actions live in `modules/tools/editor`. + +- `DeveloperToolsActionGroup` is the root editor popup group. +- `EncodeDecodeActionGroup`, `EscapeUnescapeActionGroup`, `TextCaseConverterActionGroup`, `DataGeneratorActionGroup`, and `EditorTextStatisticAction` operate mostly on selected text. +- Shared operation lists are in `EncodersDecoders`, `EscapersUnescapers`, and `DataGenerators`. +- Editor mutations should go through `EditorUtils.executeWriteCommand`. +- Error dialogs are shown via IntelliJ APIs and failures are logged. + +Generic intentions live in `modules/tools/editor/src/main/.../intention`. + +Java/Kotlin language-specific variants live in: + +- `modules/java-dependent/.../PsiJavaUtils.kt` +- `modules/java-dependent/.../tool/editor/action` +- `modules/java-dependent/.../tool/editor/intention` +- `modules/kotlin-dependent/.../PsiKotlinUtils.kt` +- `modules/kotlin-dependent/.../tool/editor/action` +- `modules/kotlin-dependent/.../tool/editor/intention` + +Those modules are optional and must stay behind their optional plugin descriptors. + +## Testing + +Common commands: + +```bash +./gradlew test +./gradlew check +./gradlew verifyPlugin +./gradlew spotlessApply +``` + +CI runs: + +```bash +./gradlew check --stacktrace +./gradlew verifyPlugin --stacktrace +``` + +Focused examples: + +```bash +./gradlew :tools-ui:test +./gradlew :settings:test +./gradlew test --tests '*PluginXmlTest' +./gradlew test --tests '*DeveloperToolsInstanceSettingsTest' +``` + +Test infrastructure: + +- `IdeaTest` sets up IntelliJ test application/project fixtures and Bouncy Castle. +- `PluginXmlTest` validates descriptor class/file references. +- `DeveloperUiToolsInstances` instantiates all registered UI tools from the extension point for integration tests. +- `DeveloperUiToolUnderTest` randomizes and resets registered tool properties. +- Intention description tests validate bundled intention descriptions/templates. + +Some tests need IntelliJ test infrastructure and can be slow. On Linux/CI, run under Xvfb as the workflow does. + +## Style And Conventions + +- Kotlin formatting is ktfmt Google style through Spotless. +- `.editorconfig` uses 2-space Kotlin indentation and 100 character max line length. +- The code often uses section comments like `// -- Properties --`; keep them when editing nearby code. +- Prefer IntelliJ Platform APIs and the local helper classes over new abstractions. +- Use IntelliJ UI DSL builder patterns already present in neighboring tools. +- Register disposables with the passed `parentDisposable`. +- Keep long or blocking work off the EDT. Existing code uses pooled threads, `Task.Backgroundable`, `AsyncTaskExecutor`, and `Alarm`. +- Many UI components rely on `ValueProperty` or IntelliJ observable properties; bind controls rather than manually syncing when possible. +- Do not change plugin ids, extension ids, or persisted property keys without migration/test updates. + +## External Processes And Downloads + +The HTTP Server tool downloads and runs WireMock standalone: + +- Implementation: `HttpServer.kt`. +- Process tracking: `ExternalSystemProcessRegistry.kt`. +- Download URL/version constants are at the bottom of `HttpServer.kt`. +- The tool supports built-in and custom server modes and registers/stops process handlers. + +Be careful with lifecycle changes here: processes must be unregistered and stopped on disposal. + +## Release And Verification Notes + +- `publishPlugin` depends on `check` and requires `platform == "idea"`. +- Signing reads certificate/private key from `~/.jetbrains` and a Gradle property password. +- Marketplace token is read from Gradle properties. +- `buildSearchableOptions` is disabled. +- Plugin verification uses recommended IDEs plus additional IDEs from `pluginVerificationAdditionalIdes`. +- Changelog rendering comes from `CHANGELOG.md`; `tools-ui` also generates a bundled `changelog.html`. + +## Agent Workflow + +Before editing: + +- Run `git status --short` and do not overwrite unrelated user changes. +- Use `rg`/`rg --files` for navigation. +- Read the closest existing implementation before adding a new pattern. + +For UI tools: + +- Start from the nearest tool in the same package. +- Wire configuration through `DeveloperToolConfiguration`. +- Register tool and enum property types in `plugin.xml`. +- Run at least `./gradlew :tools-ui:test` or the smallest relevant module test, plus `PluginXmlTest` if descriptors changed. + +For settings persistence: + +- Check `DeveloperToolsInstanceSettings`, `DeveloperToolConfiguration`, and legacy migration tests. +- Add enum registrations and migration entries as needed. +- Run `DeveloperToolsInstanceSettingsTest`. + +For editor actions/intentions: + +- Keep generic selected-text logic in `tools-editor`. +- Keep Java/Kotlin PSI logic in optional modules. +- Add/update intention descriptions under `resources/intentionDescriptions`. +- Run the relevant intention description tests. + +Before final response: + +- Run the narrowest meaningful Gradle tests. +- If descriptor or broad registration changed, run `PluginXmlTest`. +- If formatting changed, run `./gradlew spotlessApply` or `./gradlew spotlessCheck`. +- Mention any tests that could not be run. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f836ca6..3fc9c268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ ### Fixed +## 8.0.0 - 2026-05-01 + +### Added + +- Added a new HTTP Server tool for starting and managing a configurable local HTTP server based on WireMock. +- Added support for alg=none in the JWT tool. +- Added support for validating JWTs using public keys and JWKS. +- Overhauled the JWT tool UI. + +### Changed + +- Raise minimum IntelliJ version to 2026.1 + ## 7.1.0 - 2025-05-18 ### Added diff --git a/README.md b/README.md index 005f5eaa..2664470d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Plugin Logo -This plugin is a powerful and versatile set of tools designed to enhance the development experience for software engineers. With its extensive collection of features, developers can increase their productivity and simplify complex operations without leaving their coding environment. +Developer Tools brings a practical toolbox of everyday development utilities directly into IntelliJ-based IDEs. It keeps common tasks such as encoding data, transforming text, validating JSON, generating identifiers, inspecting archives, formatting code and SQL, and checking certificates inside the IDE, so you do not need to switch to separate web tools or command-line snippets. Main toolbar window: @@ -16,49 +16,51 @@ Plugin icon by [Gabriele Malaspina](https://www.svgrepo.com/svg/489187/toolbox). ## Key Features -- Encoding and Decoding: JWT (JSON Web Tokens), Base32, Base64, URL Base64, MIME Base64, URL encoding and line breaks +- JWT Encoder/Decoder +- Base32, Base64, URL Base64, MIME Base64, URL, and ASCII Encoder/Decoder +- Text escaping and unescaping for HTML entities, Java strings, JSON, CSV, XML, and escape sequences - Regular Expression Matcher -- UUID, ULID, Nano ID and Password Generator +- UUID, ULID, Nano ID, password, QR code/barcode, Lorem Ipsum, and ASCII art generators - Text Sorting - Text Case Transformation - Text Diff Viewer - Text Format Conversion -- Text Escape: HTML entities, Java Strings, JSON, CSV, and XML +- Text Statistic - Text Filter - JSON Path Parser - JSON Schema Validator -- Hashing -- Archive (ZIP, TAR, JAR, 7z, ...) viewer and extractor -- Date Time Handling (Unix Timestamp, Formatting, ...) -- Units converters for time, data size and transfer rate +- Hashing and HMAC +- HTTP Server (WireMock) +- Archive viewer and extractor for ZIP, TAR, JAR, 7z, and other formats +- Date and time tools for Unix timestamps, formatting, and parsing +- Unit converters for time, data size, and transfer rate - Code Style Formatting - SQL Formatting +- CLI Command Conversion - Color Picker -- Server certificates fetching, analyse and export -- QR Code/Barcode Generator -- Lorem Ipsum Generator -- ASCII Art +- Fetching, analyzing, and exporting server certificates +- Notes ## Integration -The main tools are currently available as a standalone dialog or tool window. Additionally, some tools are also available via the editor menu or code intentions. Some of these tools are only available if a text is selected, or the current caret position is on a Java/Kotlin string or identifier. +The full toolbox is available in both a persistent tool window and a standalone dialog. Tools can have multiple named workbenches, so you can keep separate inputs and configurations for different tasks. Frequently used text operations are also available from the editor popup menu and as intentions; depending on the action, they work on selected text or on the Java/Kotlin string or identifier at the caret. -The plugin settings can be found in IntelliJ's settings/preferences under **Tools | Developer Tools**. +Plugin settings are available in IntelliJ IDEA's settings/preferences under **Tools | Developer Tools**. ### Tool Window -The tool window is available through **View | Tool Windows | Tools**. All inputs and configurations of a tool window will be stored on the project level. +The tool window is available through **View | Tool Windows | Developer Tools**. Inputs, selected tools, expanded menu groups, and tool configuration are stored per project. ### Dialog -The action to access the dialog is available through IntelliJ's main menu under **Tools | Developer Tools**. +The dialog is available from IntelliJ IDEA's main menu under **Tools | Developer Tools**. -To add the "Open Dialog" action to the main toolbar, we can either enable it in IntelliJ's settings/preferences under **Tools | Developer Tools**, or manually add the action via **Customize Toolbar... | Add Actions... | Developer Tools**. +To add the "Open Dialog" action to the main toolbar, enable it in IntelliJ IDEA's settings/preferences under **Tools | Developer Tools**, or add it manually via **Customize Toolbar... | Add Actions... | Developer Tools**. -All inputs and configurations of the dialog will be stored on the application level. +Dialog inputs, selected tools, expanded menu groups, and tool configuration are stored at the application level. ## Development -This plugin is not seen as a library. Therefore, code changes do not necessarily adhere to the semantics version rules. +This plugin is not treated as a library, so code changes do not necessarily follow semantic versioning rules. -If you want to contribute something, please follow the code style in the `.editorconfig` and sign your commits. +If you want to contribute, please follow the code style defined in `.editorconfig` and sign your commits. diff --git a/build.gradle.kts b/build.gradle.kts index e87f2577..155016d3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ + import org.jetbrains.changelog.Changelog import org.jetbrains.intellij.platform.gradle.TestFrameworkType -import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask.FailureLevel.COMPATIBILITY_PROBLEMS import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask.FailureLevel.INTERNAL_API_USAGES import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask.FailureLevel.INVALID_PLUGIN @@ -20,38 +20,47 @@ plugins { alias(libs.plugins.version.catalog.update) } -subprojects { apply(plugin = "org.jetbrains.intellij.platform.module") } - val platform = properties("platform") allprojects { - apply(plugin = "java") - apply(plugin = "kotlin") - apply(plugin = "com.diffplug.spotless") - group = properties("pluginGroup") version = properties("pluginVersion") repositories { mavenLocal() mavenCentral() - - intellijPlatform { defaultRepositories() } } - dependencies { - intellijPlatform { - create(platform, properties("platformVersion"), false) - bundledPlugins(properties("platformGlobalBundledPlugins").split(',')) + pluginManager.withPlugin("org.jetbrains.intellij.platform.base") { + repositories { + intellijPlatform { defaultRepositories() } + } + } - testFramework(TestFrameworkType.Platform) - testFramework(TestFrameworkType.JUnit5) + pluginManager.withPlugin("org.jetbrains.intellij.platform.base") { + dependencies { + intellijPlatform { + if (platform == "idea") { + intellijIdea(properties("platformVersion")) { useInstaller = false } + } else { + create(platform, properties("platformVersion")) { useInstaller = false } + } + bundledPlugins(properties("platformGlobalBundledPlugins").split(',')) + + testFramework(TestFrameworkType.Platform) + testFramework(TestFrameworkType.JUnit5) + } } } - spotless { kotlin { ktfmt().googleStyle() } } + pluginManager.withPlugin("com.diffplug.spotless") { + spotless { kotlin { ktfmt().googleStyle() } } + } - java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } + pluginManager.withPlugin("java") { + java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } + tasks.named("check") { dependsOn("spotlessCheck") } + } configurations.all { exclude(group = "org.slf4j", module = "slf4j-api") @@ -72,8 +81,6 @@ allprojects { useJUnitPlatform() systemProperty("java.awt.headless", "false") } - - named("check") { dependsOn("spotlessCheck") } } } @@ -86,7 +93,7 @@ dependencies { pluginModule(implementation(project(":settings"))) pluginModule(implementation(project(":tools-editor"))) pluginModule(implementation(project(":tools-ui"))) - if (platform == "IC") { + if (platform == "idea") { pluginModule(implementation(project(":java-dependent"))) pluginModule(implementation(project(":kotlin-dependent"))) } @@ -152,7 +159,7 @@ intellijPlatform { recommended() properties("pluginVerificationAdditionalIdes").split(",").forEach { ide -> - ide(ide, properties("platformVersion")) + create(ide, properties("platformVersion")) } } } @@ -169,17 +176,10 @@ tasks { named("publishPlugin") { dependsOn("check") - doFirst { check(platform == "IC") { "Expected platform 'IC', but was: '$platform'" } } + doFirst { check(platform == "idea") { "Expected platform 'idea', but was: '$platform'" } } } named("buildSearchableOptions") { enabled = false } - - named("runIde") { - jvmArgumentProviders += CommandLineArgumentProvider { - // https://kotlin.github.io/analysis-api/testing-in-k2-locally.html - listOf("-Didea.kotlin.plugin.use.k2=true") - } - } } versionCatalogUpdate { diff --git a/gradle.properties b/gradle.properties index 3c6b291f..0fdcba2a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,12 @@ # The typo in the ID is frustrating, but we will never be able to change it :/ pluginId=dev.turingcomplete.intellijdevelopertoolsplugins pluginGroup=dev.turingcomplete -pluginVersion=7.1.0 +pluginVersion=8.0.0 pluginName=Developer Tools -pluginSinceBuild=243 -platform=IC +pluginSinceBuild=261 +platform=idea # LATEST-EAP-SNAPSHOT -platformVersion=2024.3 +platformVersion=2026.1 platformGlobalBundledPlugins=com.intellij.modules.json pluginVerificationAdditionalIdes=CL diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa8dad3f..a4302117 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,33 +1,33 @@ [versions] -assertj = "3.27.3" -changelog = "2.2.1" -commons-codec = "1.18.0" -commons-compress = "1.27.1" -commons-csv = "1.14.0" -commons-io = "2.19.0" -commons-text = "1.13.1" -cronutils = "9.2.0" -csscolor4j = "1.0.0" -intellij-platform = "2.5.0" -jackson = "2.19.0" +assertj = "3.27.7" +changelog = "2.5.0" +commons-codec = "1.22.0" +commons-compress = "1.28.0" +commons-csv = "1.14.1" +commons-io = "2.22.0" +commons-text = "1.15.0" +cronutils = "9.2.1" +csscolor4j = "1.1.0" +intellij-platform = "2.15.0" +jackson = "2.21.3" jfiglet = "0.0.9" jnanoid = "2.0.0" jose4j = "0.9.6" json-schema-validator = "1.5.6" -jsonpath = "2.9.0" +jsonpath = "3.0.0" junit4 = "4.13.2" -junit5 = "5.12.2" +junit5 = "6.0.3" # See bundled version: https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library -kotlin = "2.0.21" +kotlin = "2.3.0" named-regexp = "1.0.0" -okhttp = "4.12.0" -spotless = "7.0.3" -version-catalog-update = "1.0.0" +okhttp = "5.3.2" +spotless = "8.4.0" sql-formatter = "2.0.5" text-case-converter = "2.0.0" -ulid-creator = "5.2.3" -uuid-generator = "5.1.0" -zxing = "3.5.3" +ulid-creator = "5.2.4" +uuid-generator = "5.2.0" +version-catalog-update = "1.1.0" +zxing = "3.5.4" [libraries] assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b95..b1b8ef56 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793..b52fb7e7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index f5feea6d..b9bb139f 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -173,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -206,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a218..24c62d56 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -23,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/modules/common/build.gradle.kts b/modules/common/build.gradle.kts index 3517c7b0..36fe9330 100644 --- a/modules/common/build.gradle.kts +++ b/modules/common/build.gradle.kts @@ -1,6 +1,10 @@ import org.jetbrains.kotlin.gradle.utils.extendsFrom plugins { + java + alias(libs.plugins.kotlin.jvm) + id("org.jetbrains.intellij.platform.module") + alias(libs.plugins.spotless) `java-test-fixtures` } @@ -12,7 +16,7 @@ dependencies { testImplementation(libs.bundles.junit.implementation) testRuntimeOnly(libs.bundles.junit.runtime) - if (project.property("platform") == "IC") { + if (project.property("platform") == "idea") { intellijPlatform { testBundledPlugins("org.jetbrains.kotlin") } configurations.testFixturesApi.extendsFrom(configurations.intellijPlatformTestBundledPlugins) } diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/OkHttpClientUtils.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/OkHttpClientUtils.kt index 5a207365..5a9216d2 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/OkHttpClientUtils.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/OkHttpClientUtils.kt @@ -29,6 +29,7 @@ object OkHttpClientUtils { Protocol.HTTP_1_0 -> "HTTP/1.0" Protocol.HTTP_1_1 -> "HTTP/1.1" Protocol.HTTP_2 -> "HTTP/2" + Protocol.HTTP_3 -> "HTTP/3" Protocol.H2_PRIOR_KNOWLEDGE -> "HTTP/2 (Prior Knowledge)" Protocol.QUIC -> "QUIC" Protocol.SPDY_3 -> "SPDY/3.1" diff --git a/modules/common/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtilsTest.kt b/modules/common/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtilsTest.kt index b32a4c41..c2d418fc 100644 --- a/modules/common/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtilsTest.kt +++ b/modules/common/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtilsTest.kt @@ -36,24 +36,24 @@ class CryptoUtilsTest { fun validPrivateKeys(): Collection = listOf( """ - -----BEGIN PRIVATE KEY----- - MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/W - ZcdQF9i9vL+UN65wX5i2+Zm3B4TFjQtqYSbdTkCGhPrTfKZ1mH6fHnCqJce5Y4zD - UQIDAQABAkAcQ0hOM2j1dLD+Rl4nQUcxdLKHykHpeNkKccJcMqRm7C9P0hxPjTvV - cxMEk5u1bJJNV9IpTey4PPZ0ddOmFGAiAiEA8z3ibztuj9HbW1vJZTZUB3W3uyhH - 6uv3g9jPH0FcV00CIQDNzFqJk2ql+0N+2/tHkD3A0P3AUKPd0QPoRBDeyQ== - -----END PRIVATE KEY----- - """ + -----BEGIN PRIVATE KEY----- + MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/W + ZcdQF9i9vL+UN65wX5i2+Zm3B4TFjQtqYSbdTkCGhPrTfKZ1mH6fHnCqJce5Y4zD + UQIDAQABAkAcQ0hOM2j1dLD+Rl4nQUcxdLKHykHpeNkKccJcMqRm7C9P0hxPjTvV + cxMEk5u1bJJNV9IpTey4PPZ0ddOmFGAiAiEA8z3ibztuj9HbW1vJZTZUB3W3uyhH + 6uv3g9jPH0FcV00CIQDNzFqJk2ql+0N+2/tHkD3A0P3AUKPd0QPoRBDeyQ== + -----END PRIVATE KEY----- + """ .trimIndent(), """ - -----BEGIN PRIVATE KEY----- - MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/W + -----BEGIN PRIVATE KEY----- + MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/W - ZcdQF9i9vL+UN65wX5i2+Zm3B4TFjQtqYSbdTkCGhPrTfKZ1mH6fHnCqJce5Y4zD + ZcdQF9i9vL+UN65wX5i2+Zm3B4TFjQtqYSbdTkCGhPrTfKZ1mH6fHnCqJce5Y4zD - UQIDAQABAkAcQ0hOM2j1dLD+Rl4nQUcxdLKHykHpeNkKccJcMqRm7C9P0hxPjTvV - -----END PRIVATE KEY----- - """ + UQIDAQABAkAcQ0hOM2j1dLD+Rl4nQUcxdLKHykHpeNkKccJcMqRm7C9P0hxPjTvV + -----END PRIVATE KEY----- + """ .trimIndent(), "MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/W", "MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/WZcdQF9i9vL+UN65wX5i2+Zm3B4TFjQtqYSbdTkCGhPrTfKZ1mH6fHnCqJce5Y4zDUQIDAQABAkAcQ0hOM2j1dLD+Rl4nQUcxdLKHykHpeNkKccJcMqRm7C9P0hxPjTvV", @@ -64,10 +64,10 @@ class CryptoUtilsTest { fun invalidPrivateKeys(): Collection = listOf( """ - -----BEGIN PRIVATE KEY----- - InvalidBase64Content - -----END PRIVATE KEY----- - """ + -----BEGIN PRIVATE KEY----- + InvalidBase64Content + -----END PRIVATE KEY----- + """ .trimIndent(), "InvalidBase64Content", "", diff --git a/modules/java-dependent/build.gradle.kts b/modules/java-dependent/build.gradle.kts index ed743f2a..6b505905 100644 --- a/modules/java-dependent/build.gradle.kts +++ b/modules/java-dependent/build.gradle.kts @@ -1,3 +1,10 @@ +plugins { + java + alias(libs.plugins.kotlin.jvm) + id("org.jetbrains.intellij.platform.module") + alias(libs.plugins.spotless) +} + repositories { mavenLocal() mavenCentral() diff --git a/modules/kotlin-dependent/build.gradle.kts b/modules/kotlin-dependent/build.gradle.kts index 385780cb..19474aae 100644 --- a/modules/kotlin-dependent/build.gradle.kts +++ b/modules/kotlin-dependent/build.gradle.kts @@ -1,3 +1,10 @@ +plugins { + java + alias(libs.plugins.kotlin.jvm) + id("org.jetbrains.intellij.platform.module") + alias(libs.plugins.spotless) +} + dependencies { intellijPlatform { bundledPlugins("org.jetbrains.kotlin") diff --git a/modules/settings/build.gradle.kts b/modules/settings/build.gradle.kts index 07b90ea4..7fc6edd4 100644 --- a/modules/settings/build.gradle.kts +++ b/modules/settings/build.gradle.kts @@ -1,3 +1,10 @@ +plugins { + java + alias(libs.plugins.kotlin.jvm) + id("org.jetbrains.intellij.platform.module") + alias(libs.plugins.spotless) +} + dependencies { implementation(project(":common")) diff --git a/modules/tools/editor/build.gradle.kts b/modules/tools/editor/build.gradle.kts index 549d1544..ada63d1f 100644 --- a/modules/tools/editor/build.gradle.kts +++ b/modules/tools/editor/build.gradle.kts @@ -1,5 +1,12 @@ import org.gradle.kotlin.dsl.libs +plugins { + java + alias(libs.plugins.kotlin.jvm) + id("org.jetbrains.intellij.platform.module") + alias(libs.plugins.spotless) +} + dependencies { implementation(project(":common")) // This is required for the `OpenDeveloperToolService` mechanism. However, a diff --git a/modules/tools/ui/build.gradle.kts b/modules/tools/ui/build.gradle.kts index 3490731e..925a92fc 100644 --- a/modules/tools/ui/build.gradle.kts +++ b/modules/tools/ui/build.gradle.kts @@ -2,6 +2,10 @@ import org.jetbrains.changelog.Changelog plugins { + java + alias(libs.plugins.kotlin.jvm) + id("org.jetbrains.intellij.platform.module") + alias(libs.plugins.spotless) `java-test-fixtures` alias(libs.plugins.changelog) } diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AdvancedEditor.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AdvancedEditor.kt index 5eb06967..f2c2b7a6 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AdvancedEditor.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AdvancedEditor.kt @@ -12,11 +12,14 @@ import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DataProvider import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.actionSystem.Separator +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ex.ClipboardUtil import com.intellij.openapi.application.runReadAction import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener @@ -170,6 +173,27 @@ class AdvancedEditor( return this } + fun appendText(value: String) { + if (value.isEmpty()) { + return + } + + updateDocument { + val document = editor.document + document.insertString(document.textLength, value) + editor.caretModel.moveToOffset(document.textLength) + editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE) + } + } + + fun clearText() { + updateDocument { + editor.document.setText("") + editor.caretModel.moveToOffset(0) + editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE) + } + } + fun onFocusGained(changeListener: () -> Unit): AdvancedEditor { onFocusGained = changeListener return this @@ -406,6 +430,23 @@ class AdvancedEditor( } } + private fun updateDocument(update: () -> Unit) { + val application = ApplicationManager.getApplication() + val action = Runnable { + if (editor.isDisposed) { + return@Runnable + } + + CommandProcessor.getInstance().runUndoTransparentAction { runWriteAction(update) } + } + + if (application.isDispatchThread) { + action.run() + } else { + application.invokeLater(action) + } + } + private fun EditorEx.syncEditorColors() { setBackgroundColor(null) // To use background from set color scheme diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPane.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPane.kt index d1a7e9da..e2c31c5a 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPane.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPane.kt @@ -3,7 +3,6 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.ide.ui.laf.darcula.ui.DarculaTabbedPaneUI import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBTabbedPane -import com.intellij.util.ui.JBUI import java.awt.Font import java.awt.Graphics import javax.swing.JComponent @@ -14,13 +13,13 @@ class TitledTabbedPane(title: String, tabs: List>) : JB // -- Initialization ------------------------------------------------------ // init { - tabComponentInsets = JBUI.insetsTop(8) + applyDefaultTabComponentInsets() addTab("", JPanel()) setTabComponentAt(0, JBLabel(title).apply { font = font?.deriveFont(Font.BOLD) }) setEnabledAt(0, false) - tabs.forEach { addTab(it.first, it.second) } + tabs.forEach { addTab(it.first, it.second.wrapTabbedPaneContent()) } selectedIndex = 1 setUI(TitleTabAwareTabbedPaneUi()) diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiExtensions.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiExtensions.kt index 42594f46..4a8dd5a6 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiExtensions.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiExtensions.kt @@ -9,6 +9,9 @@ import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.getUserData +import com.intellij.openapi.ui.putUserData +import com.intellij.openapi.util.Key import com.intellij.ui.HyperlinkLabel import com.intellij.ui.JBColor import com.intellij.ui.UIBundle @@ -24,6 +27,7 @@ import com.intellij.ui.tabs.TabInfo import com.intellij.util.ui.JBEmptyBorder import com.intellij.util.ui.JBFont import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.Color import java.awt.Font @@ -214,6 +218,16 @@ fun JComponent.wrapWithToolBar( } } +fun JComponent.wrapTabbedPaneContent(topInset: Int = 8): JComponent { + return BorderLayoutPanel().apply { + isOpaque = true + background = UIUtil.getPanelBackground() + border = JBUI.Borders.emptyTop(topInset) + putUserData(wrappedTabbedPaneContentKey, this@wrapTabbedPaneContent) + addToCenter(this@wrapTabbedPaneContent) + } +} + fun JComponent.withNoRightBorderInset(): JComponent { val insets = border?.getBorderInsets(this) ?: JBUI.emptyInsets() border = JBUI.Borders.empty(insets.top, insets.left, insets.bottom, 0) @@ -287,11 +301,20 @@ fun Cell.registerDynamicToolTip(toolTipText: () -> String?) { } fun JBTabbedPane.onSelectionChanged(onSelectionChanged: (JComponent) -> Unit): JBTabbedPane { - addChangeListener { onSelectionChanged(selectedComponent as JComponent) } + addChangeListener { + val selectedComponent = selectedComponent as? JComponent ?: return@addChangeListener + onSelectionChanged( + selectedComponent.getUserData(wrappedTabbedPaneContentKey) ?: selectedComponent + ) + } return this } +fun JBTabbedPane.applyDefaultTabComponentInsets() { + tabComponentInsets = JBUI.insetsTop(5) +} + // -- Private Methods ---------------------------------------------------- // // -- Inner Type ---------------------------------------------------------- // @@ -308,3 +331,5 @@ enum class ToolBarPlace(val horizontal: Boolean) { RIGHT(false), APPEND(true), } + +private val wrappedTabbedPaneContentKey = Key.create("wrappedTabbedPaneContent") diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/JwtEncoderDecoder.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/JwtEncoderDecoder.kt deleted file mode 100644 index 44b826d4..00000000 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/JwtEncoderDecoder.kt +++ /dev/null @@ -1,1114 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.ObjectNode -import com.intellij.icons.AllIcons -import com.intellij.json.JsonLanguage -import com.intellij.lang.Language -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.editor.colors.EditorColors -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.markup.GutterIconRenderer -import com.intellij.openapi.editor.markup.HighlighterLayer -import com.intellij.openapi.fileTypes.PlainTextLanguage -import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.ui.Splitter -import com.intellij.openapi.util.TextRange -import com.intellij.ui.IconManager -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.LabelPosition -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.RowLayout -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.actionButton -import com.intellij.ui.dsl.builder.bindItem -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.dsl.builder.rows -import com.intellij.ui.dsl.builder.selected -import com.intellij.ui.dsl.builder.whenItemSelectedFromUi -import com.intellij.ui.dsl.builder.whenStateChangedFromUi -import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import com.intellij.ui.layout.ComboBoxPredicate -import com.intellij.ui.layout.not -import com.intellij.util.Alarm -import com.intellij.util.ExceptionUtil -import com.intellij.util.text.DateFormatUtil -import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin.common.capitalize -import dev.turingcomplete.intellijdevelopertoolsplugin.common.decodeBase64String -import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.SENSITIVE -import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings -import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.Companion.createSensitiveInputsHandlingToolTipText -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.ObjectMapperService -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.SimpleToggleAction -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.registerDynamicToolTip -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.setValidationResultBorder -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.ChangeOrigin.ENCODED -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.ChangeOrigin.HEADER_OR_PAYLOAD -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.ChangeOrigin.SIGNATURE_CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SecretKeyEncodingMode.BASE32 -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SecretKeyEncodingMode.BASE64 -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SecretKeyEncodingMode.RAW -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithm.HMAC256 -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithmKind.ECDSA -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithmKind.HMAC -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithmKind.RSA -import java.security.Key -import java.security.KeyFactory -import java.security.spec.PKCS8EncodedKeySpec -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.util.Base64 -import java.util.Objects -import java.util.StringJoiner -import javax.swing.Icon -import javax.swing.JComponent -import org.apache.commons.codec.binary.Base32 -import org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256 -import org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384 -import org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512 -import org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA256 -import org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA384 -import org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA512 -import org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA256 -import org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA384 -import org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA512 -import org.jose4j.jws.JsonWebSignature -import org.jose4j.keys.HmacKey - -class JwtEncoderDecoder( - private val context: DeveloperUiToolContext, - private val configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - private val project: Project?, -) : DeveloperUiTool(parentDisposable) { - // -- Properties ---------------------------------------------------------- // - - private var liveConversion = configuration.register("liveConversion", true) - private var encodedText = configuration.register("encodedText", "", INPUT, EXAMPLE_ENCODED) - private var headerText = configuration.register("headerText", "", INPUT, EXAMPLE_HEADER) - private var payloadText = configuration.register("payloadText", "", INPUT, EXAMPLE_PAYLOAD) - - private val highlightEncodedAlarm by lazy { Alarm(parentDisposable) } - private val highlightHeaderAlarm by lazy { Alarm(parentDisposable) } - private val highlightPayloadAlarm by lazy { Alarm(parentDisposable) } - private val conversionAlarm by lazy { Alarm(parentDisposable) } - - private var lastActiveInput: AdvancedEditor? = null - private val encodedEditor by lazy { createEncodedEditor() } - private val headerEditor by lazy { createHeaderEditor() } - private val payloadEditor by lazy { createPayloadEditor() } - - private val highlightingAttributes by lazy { - EditorColorsManager.getInstance() - .globalScheme - .getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES) - } - - private val jwt = Jwt(configuration, encodedText, headerText, payloadText) - - // -- Initialization ------------------------------------------------------ // - - init { - liveConversion.afterChange(parentDisposable) { handleLiveConversionSwitch() } - - jwt.signature.secretEncodingMode.afterChangeConsumeEvent(null) { e -> - if (e.valueChanged()) { - convertFromUi(SIGNATURE_CONFIGURATION) - } - } - } - - // -- Exposed Methods ----------------------------------------------------- // - - override fun Panel.buildUi() { - row { - cell( - Splitter(true, 0.2f).apply { - firstComponent = createEncodedComponent() - secondComponent = createEncodingDecodingComponent() - } - ) - .align(Align.FILL) - .resizableColumn() - } - .resizableRow() - } - - private fun createEncodedComponent(): JComponent = panel { - row { - cell(encodedEditor.component) - .validationOnApply(encodedEditor.bindValidator(jwt.encodedErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .align(Align.FILL) - .resizableColumn() - } - .resizableRow() - .bottomGap(BottomGap.NONE) - val signatureErrors = jwt.signatureErrorHolder.asComponentPredicate() - row { - text(" Valid signature") - .visibleIf(signatureErrors.not()) - .resizableColumn() - text("") - .bindText(jwt.signatureErrorHolder.asPropertyForTextCell()) - .visibleIf(signatureErrors) - .resizableColumn() - } - .topGap(TopGap.NONE) - } - - @Suppress("UnstableApiUsage") - private fun createEncodingDecodingComponent(): JComponent = panel { - row { - val liveConversionCheckBox = - checkBox("Live conversion").bindSelected(liveConversion).gap(RightGap.SMALL) - - button("▼ Decode") { convert(ENCODED) } - .enabledIf(liveConversionCheckBox.selected.not()) - .gap(RightGap.SMALL) - button("▲ Encode") { - // This will set the signature algorithm in the header and will run the encoding. - convert(SIGNATURE_CONFIGURATION) - } - .enabledIf(liveConversionCheckBox.selected.not()) - } - - if (context.prioritizeVerticalLayout) { - row { - cell( - Splitter(true, 0.3f).apply { - firstComponent = createHeaderEditorComponent() - secondComponent = createPayloadEditorComponent() - } - ) - .align(Align.FILL) - .resizableColumn() - } - .resizableRow() - .bottomGap(BottomGap.NONE) - } else { - row { - cell( - Splitter(false, 0.5f).apply { - firstComponent = createHeaderEditorComponent() - secondComponent = createPayloadEditorComponent() - } - ) - .align(Align.FILL) - .resizableColumn() - } - .resizableRow() - .bottomGap(BottomGap.NONE) - } - - collapsibleGroup("Signature Algorithm Configuration") { - lateinit var signatureAlgorithmComboBox: ComboBox - row { - signatureAlgorithmComboBox = - comboBox(SignatureAlgorithm.entries) - .label("Algorithm:") - .bindItem(jwt.signature.algorithm) - .whenItemSelectedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } - .component - } - .layout(RowLayout.PARENT_GRID) - .topGap(TopGap.NONE) - - row { - // Bug: The label from `expandableTextField().label(...)` disappears - // if the encoding selection gets changed - label("Secret key:") - expandableTextField() - .align(AlignX.FILL) - .bindText(jwt.signature.secret) - .whenTextChangedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } - .gap(RightGap.SMALL) - .resizableColumn() - .registerDynamicToolTip { generalSettings.createSensitiveInputsHandlingToolTipText() } - - val encodingActions = - mutableListOf().apply { - SecretKeyEncodingMode.entries.forEach { secretKeyEncodingModeValue -> - add( - SimpleToggleAction( - text = secretKeyEncodingModeValue.title, - icon = AllIcons.Actions.ToggleSoftWrap, - isSelected = { - jwt.signature.secretEncodingMode.get() == secretKeyEncodingModeValue - }, - setSelected = { - jwt.signature.secretEncodingMode.set(secretKeyEncodingModeValue) - }, - ) - ) - } - } - actionButton( - UiUtils.actionsPopup( - title = "Encoding", - icon = AllIcons.General.Settings, - actions = encodingActions, - ) - ) - } - .visibleIf(ComboBoxPredicate(signatureAlgorithmComboBox) { it?.kind?.keyFactory == null }) - .layout(RowLayout.PARENT_GRID) - - row { - textArea() - .rows(5) - .align(Align.FILL) - .label(label = "Private key:", position = LabelPosition.TOP) - .bindText(jwt.signature.privateKey) - .setValidationResultBorder() - .whenTextChangedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } - .validationInfo(jwt.signature.privateKeyErrorHolder.asValidation()) - .registerDynamicToolTip { generalSettings.createSensitiveInputsHandlingToolTipText() } - } - .visibleIf(ComboBoxPredicate(signatureAlgorithmComboBox) { it?.kind?.keyFactory != null }) - - row { - checkBox("Strict key requirements validation") - .bindSelected(jwt.signature.strictSigningKeyValidation) - .whenStateChangedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } - .gap(RightGap.SMALL) - contextHelp( - "The RFC 7518 for the JSON Web Algorithms (JWA) specifies some restrictions that a key or secret should fulfill for the computation of a signature (e.g., a minimum length). This option can be used to enforce these restrictions." - ) - } - } - .apply { expanded = false } - .topGap(TopGap.NONE) - } - - private fun createPayloadEditorComponent(): JComponent = panel { - row { - cell(payloadEditor.component) - .validationOnApply(payloadEditor.bindValidator(jwt.payloadErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .align(Align.FILL) - .resizableColumn() - } - .resizableRow() - } - - private fun createHeaderEditorComponent(): JComponent = panel { - row { - cell(headerEditor.component) - .validationOnApply(headerEditor.bindValidator(jwt.headerErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .align(Align.FILL) - .resizableColumn() - } - .resizableRow() - } - - override fun afterBuildUi() { - convertFromUi(ENCODED) - } - - override fun reset() { - convert(ENCODED) - } - - // -- Private Methods ----------------------------------------------------- // - - private fun convertFromUi(changeOrigin: ChangeOrigin) { - if (!liveConversion.get()) { - return - } - - convert(changeOrigin) - } - - private fun convert(changeOrigin: ChangeOrigin) { - if (configuration.isResetting) { - return - } - - if (!isDisposed && !conversionAlarm.isDisposed) { - conversionAlarm.cancelAllRequests() - conversionAlarm.addRequest({ doConvert(changeOrigin) }, 100) - } - } - - private fun doConvert(changeOrigin: ChangeOrigin) { - when (changeOrigin) { - ENCODED -> jwt.decodeJwt() - HEADER_OR_PAYLOAD -> jwt.encodeJwt() - SIGNATURE_CONFIGURATION -> { - jwt.setAlgorithmInHeader() - jwt.encodeJwt() - } - } - - highlightDotSeparator() - highlightHeaderClaims() - highlightPayloadClaims() - - // The `validate` in this class is not used as a validation mechanism. We - // make use of its text field error UI to display the `errorHolder`. - validate() - } - - private fun highlightDotSeparator() { - val highlightDotSeparator = { - encodedEditor.removeTextRangeHighlighters(ENCODED_DOT_SEPARATOR_GROUP_ID) - val encoded = encodedText.get() - var dotIndex = encoded.indexOf('.') - var i = 0 - while (dotIndex != -1) { - encodedEditor.highlightTextRange( - TextRange(dotIndex, dotIndex + 1), - ENCODED_DOT_SEPARATOR_HIGHLIGHTER_LAYER, - highlightingAttributes, - ENCODED_DOT_SEPARATOR_GROUP_ID, - ) - dotIndex = encoded.indexOf('.', dotIndex + 1) - i++ - } - } - if (!isDisposed && !highlightEncodedAlarm.isDisposed) { - highlightEncodedAlarm.cancelAllRequests() - highlightEncodedAlarm.addRequest(highlightDotSeparator, 100) - } - } - - private fun highlightHeaderClaims() { - if (!isDisposed && !highlightHeaderAlarm.isDisposed) { - highlightHeaderAlarm.cancelAllRequests() - highlightHeaderAlarm.addRequest({ doHighlightClaims(headerEditor) }, 100) - } - } - - private fun highlightPayloadClaims() { - if (!isDisposed && !highlightPayloadAlarm.isDisposed) { - highlightPayloadAlarm.cancelAllRequests() - highlightPayloadAlarm.addRequest({ doHighlightClaims(payloadEditor) }, 100) - } - } - - private fun doHighlightClaims(editor: AdvancedEditor) { - editor.removeTextRangeHighlighters(HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID) - - UNIX_TIMESTAMP_SECONDS_JSON_VALUE_REGEX.findAll(editor.text).forEach { - val unixTimestampSecondsMatch = it.groups[1] - if (unixTimestampSecondsMatch != null) { - val textRange = - TextRange(unixTimestampSecondsMatch.range.first, unixTimestampSecondsMatch.range.last + 1) - editor.highlightTextRange( - textRange, - UNIX_TIMESTAMP_HIGHLIGHT_LAYER, - null, - HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID, - ClaimFeatureUnixTimestampGutterIconRenderer( - textRange, - unixTimestampSecondsMatch.value.toLong(), - ), - ) - } - } - - CLAIM_REGEX.findAll(editor.text) - .mapNotNull { - val claimMatch = it.groups[1] - if (claimMatch != null) { - val standardClaim = StandardClaim.findByFieldName(claimMatch.value) - if (standardClaim != null) { - return@mapNotNull claimMatch.range to standardClaim - } - } - null - } - .forEach { (claimRange, standardClaim) -> - val textRange = TextRange(claimRange.first, claimRange.last + 1) - editor.highlightTextRange( - textRange, - CLAIM_REGEX_MATCH_HIGHLIGHT_LAYER, - null, - HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID, - StandardClaimGutterIconRenderer(textRange, standardClaim), - ) - } - } - - private fun createEncodedEditor(): AdvancedEditor = - createEditor( - id = "encoded", - changeOrigin = ENCODED, - title = "Encoded", - language = PlainTextLanguage.INSTANCE, - textProperty = encodedText, - ) { - highlightDotSeparator() - } - - private fun createHeaderEditor(): AdvancedEditor = - createEditor( - id = "header", - changeOrigin = HEADER_OR_PAYLOAD, - title = "Header", - language = JsonLanguage.INSTANCE, - textProperty = headerText, - ) { - highlightHeaderClaims() - } - - private fun createPayloadEditor(): AdvancedEditor = - createEditor( - id = "payload", - changeOrigin = HEADER_OR_PAYLOAD, - title = "Payload", - language = JsonLanguage.INSTANCE, - textProperty = payloadText, - ) { - highlightPayloadClaims() - } - - private fun createEditor( - id: String, - changeOrigin: ChangeOrigin, - title: String, - language: Language, - textProperty: ValueProperty, - onTextChangeFromUi: (() -> Unit)? = null, - ) = - AdvancedEditor( - id = id, - context = context, - configuration = configuration, - project = project, - title = title, - editorMode = EditorMode.INPUT_OUTPUT, - parentDisposable = parentDisposable, - textProperty = textProperty, - initialLanguage = language, - ) - .apply { - onFocusGained { lastActiveInput = this } - this.onTextChangeFromUi { _ -> - lastActiveInput = this - convertFromUi(changeOrigin) - onTextChangeFromUi?.invoke() - } - } - - private fun handleLiveConversionSwitch() { - if (liveConversion.get()) { - // Trigger a text change. So if the text was changed in manual mode, it - // will now be encoded/decoded once during the switch to live mode. - when (lastActiveInput) { - encodedEditor -> convert(ENCODED) - headerEditor, - payloadEditor -> { - // This will set the signature algorithm in the header and will run the encoding. - convert(SIGNATURE_CONFIGURATION) - } - - null -> {} - } - } - } - - // -- Inner Type ---------------------------------------------------------- // - - private class StandardClaimGutterIconRenderer( - private val textRange: TextRange, - private val standardClaim: StandardClaim, - ) : GutterIconRenderer(), DumbAware { - - override fun getTooltipText(): String = standardClaim.toString() - - override fun getIcon(): Icon = AllIcons.Gutter.JavadocRead - - override fun equals(other: Any?): Boolean { - return if (other != null && other is StandardClaimGutterIconRenderer) { - other.textRange == textRange && other.standardClaim == standardClaim - } else { - false - } - } - - override fun hashCode(): Int = Objects.hash(textRange, standardClaim) - - override fun getAlignment(): Alignment = Alignment.RIGHT - } - - // -- Inner Type ---------------------------------------------------------- // - - private class ClaimFeatureUnixTimestampGutterIconRenderer( - private val textRange: TextRange, - private val unixTimestampSeconds: Long, - ) : GutterIconRenderer(), DumbAware { - - override fun getTooltipText(): String { - val tooltipText = StringJoiner("

") - - tooltipText.add( - Instant.ofEpochSecond(unixTimestampSeconds) - .atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ISO_ZONED_DATE_TIME) - ) - - val diff = - DateFormatUtil.formatBetweenDates( - unixTimestampSeconds.times(1000), - System.currentTimeMillis(), - ) - tooltipText.add("${diff.capitalize()}.") - - return tooltipText.toString() - } - - override fun getIcon(): Icon = clockGutterIcon - - override fun equals(other: Any?): Boolean { - return if (other != null && other is ClaimFeatureUnixTimestampGutterIconRenderer) { - other.textRange == textRange && other.unixTimestampSeconds == unixTimestampSeconds - } else { - false - } - } - - override fun hashCode(): Int = Objects.hash(textRange, unixTimestampSeconds) - - override fun getAlignment(): Alignment = Alignment.LEFT - - companion object { - - private val clockGutterIcon = - IconManager.getInstance() - .getIcon( - "dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter.svg", - JwtEncoderDecoder::class.java.classLoader, - ) - } - } - - // -- Inner Type ---------------------------------------------------------- // - - private enum class ChangeOrigin { - - ENCODED, - HEADER_OR_PAYLOAD, - SIGNATURE_CONFIGURATION, - } - - // -- Inner Type ---------------------------------------------------------- // - - enum class SignatureAlgorithmKind(val keyFactory: KeyFactory?) { - - HMAC(null), - RSA(KeyFactory.getInstance("RSA")), - ECDSA(KeyFactory.getInstance("EC")), - } - - // -- Inner Type ---------------------------------------------------------- // - - enum class SignatureAlgorithm( - val jwtHeaderValue: String, - val kind: SignatureAlgorithmKind, - @Suppress("unused") // May be used for JWK validation - val algorithmIdentifiers: String, - ) { - - HMAC256("HS256", HMAC, HMAC_SHA256), - HMAC384("HS384", HMAC, HMAC_SHA384), - HMAC512("HS512", HMAC, HMAC_SHA512), - RSA256("RS256", RSA, RSA_USING_SHA256), - RSA384("RS384", RSA, RSA_USING_SHA384), - RSA512("RS512", RSA, RSA_USING_SHA512), - ECDSA256("ES256", ECDSA, ECDSA_USING_P256_CURVE_AND_SHA256), - ECDSA384("ES384", ECDSA, ECDSA_USING_P384_CURVE_AND_SHA384), - ECDSA512("ES512", ECDSA, ECDSA_USING_P521_CURVE_AND_SHA512); - - override fun toString(): String = "$name ($jwtHeaderValue)" - - companion object { - - fun findByJwtHeaderValue(jwtHeaderValue: String): SignatureAlgorithm? = - entries.firstOrNull { it.jwtHeaderValue == jwtHeaderValue } - } - } - - // -- Inner Type ---------------------------------------------------------- // - - private class Jwt( - configuration: DeveloperToolConfiguration, - val encoded: ValueProperty, - val header: ValueProperty, - val payload: ValueProperty, - ) { - - val encodedErrorHolder = ErrorHolder() - val headerErrorHolder = ErrorHolder() - val payloadErrorHolder = ErrorHolder() - val signatureErrorHolder = - ErrorHolder(addErrorIconToMessage = true, surroundMessageWithHtml = false) - - val signature = Signature(configuration, signatureErrorHolder) - - fun decodeJwt() { - clearErrorHolders() - - val jwtParts = encoded.get().split(".") - val numOfJwtParts = jwtParts.size - - // Header - if (numOfJwtParts >= 1) { - val handleError: (Exception) -> Unit = { error -> - header.set(jwtParts[0]) - headerErrorHolder.add(error) - } - parseAsJson(text = jwtParts[0], textIsBase64 = true, handleError) { - parseHeader(it) - header.set(ObjectMapperService.instance.prettyPrintJson(it)) - } - } else { - header.set("") - } - - // Payload - if (numOfJwtParts >= 2) { - val handleError: (Exception) -> Unit = { error -> - payload.set(jwtParts[1]) - payloadErrorHolder.add(error) - } - parseAsJson(text = jwtParts[1], textIsBase64 = true, handleError) { - payload.set(ObjectMapperService.instance.prettyPrintJson(it)) - } - } else { - payload.set("") - } - - // Signature - if (numOfJwtParts >= 3 && headerErrorHolder.isNotSet()) { - signature.compute(jwtParts[0], jwtParts[1])?.let { expectedSignature -> - val actualSignature = jwtParts[2] - if (expectedSignature != actualSignature) { - signatureErrorHolder.add( - "Invalid signature. Check the configuration in the 'Signature Algorithm Configuration' section." - ) - } - } - } else { - signatureErrorHolder.add("Encoded JWT does not have a signature part") - } - } - - fun encodeJwt() { - clearErrorHolders() - - val jsonMapper = ObjectMapperService.instance.jsonMapper() - - val headerJson = - try { - val headerJson = jsonMapper.readTree(header.get()) - parseHeader(headerJson) - headerJson - } catch (e: Exception) { - headerErrorHolder.add(e) - null - } - - val payloadJson = - try { - jsonMapper.readTree(payload.get()) - } catch (e: Exception) { - payloadErrorHolder.add(e) - null - } - - if (headerErrorHolder.isSet() || payloadErrorHolder.isSet()) { - encoded.set("") - signatureErrorHolder.add("Unable to compute signature due to header or payload errors") - } else { - val encodedHeader = - urlEncoder - .encode(jsonMapper.writeValueAsString(headerJson!!).encodeToByteArray()) - .decodeToString() - val encodedPayload = - urlEncoder - .encode(jsonMapper.writeValueAsString(payloadJson!!).encodeToByteArray()) - .decodeToString() - val encodedSignature = signature.compute(encodedHeader, encodedPayload) - if (encodedSignature == null) { - encoded.set("") - signatureErrorHolder.addIfNoErrors( - "Unable to compute signature due signature configuration errors" - ) - } else { - encoded.set("${encodedHeader}.${encodedPayload}.$encodedSignature") - } - } - } - - fun setAlgorithmInHeader() { - parseAsJson(text = header.get(), textIsBase64 = false, { headerErrorHolder.add(it) }) { - headerNode -> - if (headerNode is ObjectNode) { - headerNode.put("alg", signature.algorithm.get().jwtHeaderValue) - header.set(ObjectMapperService.instance.prettyPrintJson(headerNode)) - } - } - } - - private fun clearErrorHolders() { - encodedErrorHolder.clear() - headerErrorHolder.clear() - payloadErrorHolder.clear() - signatureErrorHolder.clear() - } - - private fun parseHeader(headerNode: JsonNode) { - if (headerNode.has("alg")) { - val algFieldValue = headerNode.get("alg").asText() - val algorithm = SignatureAlgorithm.findByJwtHeaderValue(algFieldValue) - if (algorithm != null) { - signature.algorithm.set(algorithm) - } else { - headerErrorHolder.add("Unsupported algorithm: '$algFieldValue'") - } - } else { - headerErrorHolder.add("Missing algorithm header field: 'alg'") - } - } - - private fun parseAsJson( - text: String, - textIsBase64: Boolean, - handleError: (Exception) -> Unit, - handleResult: (JsonNode) -> Unit, - ) { - try { - val actualText = if (textIsBase64) text.decodeBase64String() else text - val jsonNode = ObjectMapperService.instance.jsonMapper().readTree(actualText) - handleResult(jsonNode) - } catch (e: Exception) { - handleError(e) - } - } - } - - // -- Inner Type ---------------------------------------------------------- // - - private class Signature( - configuration: DeveloperToolConfiguration, - private val signatureErrorHolder: ErrorHolder, - ) { - - val algorithm = configuration.register("algorithm", DEFAULT_SIGNATURE_ALGORITHM) - val strictSigningKeyValidation = - configuration.register("signingKeyValidation", SIGNING_KEY_VALIDATION_DEFAULT, CONFIGURATION) - val secret = configuration.register("secret", "", SENSITIVE, EXAMPLE_SECRET) - val privateKey = - configuration.registerWithExampleProvider("privateKey", "", SENSITIVE) { - if (algorithm.get().kind == RSA) EXAMPLE_RSA_PRIVATE_KEY else EXAMPLE_EC_PRIVATE_KEY - } - val secretEncodingMode = configuration.register("secretKeyEncodingMode", RAW, CONFIGURATION) - - val privateKeyErrorHolder = ErrorHolder() - - init { - handleAlgorithmChange() - algorithm.afterChangeConsumeEvent(null) { e -> - if (e.valueChanged()) { - handleAlgorithmChange() - } - } - } - - fun compute(encodedHeader: String, encodedPayload: String): String? { - privateKeyErrorHolder.clear() - - return try { - val signingKey = createSigningKey() ?: return null - ExtendedJsonWebSignature() - .apply { - // The algorithm gets derivative from the header property `alg` - setEncodedHeader(encodedHeader) - setEncodedPayload(encodedPayload) - setKey(signingKey) - isDoKeyValidation = strictSigningKeyValidation.get() - sign() - } - .encodedSignature - } catch (e: Exception) { - signatureErrorHolder.add("Failed to compute signature:", ExceptionUtil.getRootCause(e)) - null - } - } - - private fun createSigningKey(): Key? { - val signatureAlgorithm = algorithm.get() - return when (signatureAlgorithm.kind) { - HMAC -> - HmacKey( - when (secretEncodingMode.get()) { - RAW -> secret.get().encodeToByteArray() - BASE32 -> Base32().decode(secret.get()) - BASE64 -> Base64.getDecoder().decode(secret.get()) - } - ) - - RSA, - ECDSA -> readPrivateKey(signatureAlgorithm.kind.keyFactory!!) ?: return null - } - } - - private fun readPrivateKey(keyFactory: KeyFactory) = - try { - val privateKeyValue = privateKey.get() - if (privateKeyValue.isBlank()) { - privateKeyErrorHolder.add("A private key must be provided") - null - } else { - keyFactory.generatePrivate(PKCS8EncodedKeySpec(toRawKey(privateKey.get()))) - } - } catch (e: Exception) { - privateKeyErrorHolder.add(e) - null - } - - private fun toRawKey(keyInput: String): ByteArray = - Base64.getDecoder().decode(keyInput.replace(RAW_KEY_REGEX, "")) - - private fun handleAlgorithmChange() { - if (generalSettings.loadExamples.get()) { - loadExampleSecrets() - } - } - - private fun loadExampleSecrets() { - val privateKeyValue = privateKey.get() - when (algorithm.get().kind) { - HMAC -> { - if (secret.get().isBlank()) { - secret.set(EXAMPLE_SECRET) - } - } - - RSA -> { - if (privateKeyValue.isBlank() || privateKeyValue == EXAMPLE_EC_PRIVATE_KEY) { - privateKey.set(EXAMPLE_RSA_PRIVATE_KEY) - } - } - - ECDSA -> { - if (privateKeyValue.isBlank() || privateKeyValue == EXAMPLE_RSA_PRIVATE_KEY) { - privateKey.set(EXAMPLE_EC_PRIVATE_KEY) - } - } - } - } - } - - // -- Inner Type ---------------------------------------------------------- // - - private enum class StandardClaim( - val fieldName: String, - val title: String, - val description: String, - ) { - - TYP(fieldName = "typ", title = "Type", description = "Indicating that this token is a JWT."), - ISSUER(fieldName = "iss", title = "Issuer", description = "The issuer of the JWT."), - SUBJECT(fieldName = "sub", title = "Subject", description = "The subject of the JWT."), - AUDIENCE(fieldName = "aud", title = "Audience", description = "The recipient of the JWT."), - EXPIRATION_TIME( - fieldName = "exp", - title = "Expiration Time", - description = "JWT expiration time.", - ), - NOT_BEFORE_TIME( - fieldName = "nbf", - title = "Not Before Time", - description = "JWT valid after this time.", - ), - ISSUED_AT_TIME( - fieldName = "iat", - title = "Issued at Time", - description = "JWT issued at this time.", - ), - JWT_ID(fieldName = "jti", title = "JWT ID", description = "A unique identifier for the JWT."), - ALG( - fieldName = "alg", - title = "Algorithm", - description = "The algorithm to calculate the signature of this JWT.", - ), - AZP( - fieldName = "azp", - title = "Authorized Party", - description = "The party to which the JWT was issued.", - ), - SID(fieldName = "sid", title = "Session ID", description = "An unique session ID."), - NONCE( - fieldName = "nonce", - title = "Nonce", - description = "A value used to associate a client session with this JWT.", - ), - AT_HASH( - fieldName = "at_hash", - title = "Access Token Hash Value", - description = "The hash of an access token.", - ), - C_HASH(fieldName = "c_hash", title = "Code Hash Value", description = "The hash of a code."), - ACT(fieldName = "act", title = "Actor", description = "The has of an access token."), - AUTH_TIME( - fieldName = "auth_time", - title = "Authentication Time", - description = "Time of user authentication.", - ), - SCOPE(fieldName = "scope", title = "Scope", description = "Permissions granted to the token."); - - override fun toString(): String = "$title ($fieldName)
$description" - - companion object { - - fun findByFieldName(fieldName: String): StandardClaim? = - entries.firstOrNull { it.fieldName == fieldName } - } - } - - // -- Inner Type ---------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = - DeveloperUiToolPresentation( - menuTitle = "JSON Web Token (JWT)", - contentTitle = "JSON Web Token (JWT) Decoder/Encoder", - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - ): ((DeveloperToolConfiguration) -> JwtEncoderDecoder) = { configuration -> - JwtEncoderDecoder(context, configuration, parentDisposable, project) - } - } - - // -- Inner Type ---------------------------------------------------------- // - - // -- Inner Type ---------------------------------------------------------- // - - private class ExtendedJsonWebSignature : JsonWebSignature() { - - @Suppress("RedundantVisibilityModifier") // False-positive - public override fun setEncodedHeader(encodedHeader: String?) { - super.setEncodedHeader(encodedHeader) - } - } - - // -- Inner Type ---------------------------------------------------------- // - - enum class SecretKeyEncodingMode(val title: String) { - - RAW("Raw"), - BASE32("Base32 Encoded"), - BASE64("Base64 Encoded"), - } - - // -- Companion Object ---------------------------------------------------- // - - companion object { - - private val urlEncoder = Base64.getUrlEncoder().withoutPadding() - - private val UNIX_TIMESTAMP_SECONDS_JSON_VALUE_REGEX = - Regex(":\\s*(?\\b\\d{1,10}\\b)") - private val CLAIM_REGEX = Regex("\\s?\"(?[a-zA-Z]+)\"\\s?:") - private const val UNIX_TIMESTAMP_HIGHLIGHT_LAYER = HighlighterLayer.SELECTION - 1 - private const val CLAIM_REGEX_MATCH_HIGHLIGHT_LAYER = UNIX_TIMESTAMP_HIGHLIGHT_LAYER - 1 - - private const val HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID = "claims" - private const val ENCODED_DOT_SEPARATOR_GROUP_ID = "encodedDotSeparator" - private const val ENCODED_DOT_SEPARATOR_HIGHLIGHTER_LAYER = HighlighterLayer.SELECTION - 1 - - private val RAW_KEY_REGEX = Regex("\\r?\\n|\\r|\\s?-+(BEGIN|END).*KEY-+\\s?") - - private val DEFAULT_SIGNATURE_ALGORITHM = HMAC256 - private const val EXAMPLE_ENCODED = - "ewogICJ0eXAiOiJKV1QiLAogICJhbGciOiJIUzI1NiIKfQ.ewogICJqdGkiOiI5NjQ5MmQ1OS0wYWQ1LTRjMDAtODkyZC01OTBhZDVhYzAwZjMiLAogICJzdWIiOiIwMTIzNDU2Nzg5IiwKICAibmFtZSI6IkpvaG4gRG9lIiwKICAiaWF0IjoxNjgxMDQwNTE1Cn0.IqeNl3lHSUfPfEYmttvlQp1sH9LpAoPJlUiSv4XPDSE" - private const val EXAMPLE_SECRET = "s3cre!" - private val EXAMPLE_HEADER = - """ - { - "typ":"JWT", - "alg":"HS256" - } - """ - .trimIndent() - private val EXAMPLE_PAYLOAD = - """ - { - "jti":"96492d59-0ad5-4c00-892d-590ad5ac00f3", - "sub":"0123456789", - "name":"John Doe", - "iat":1681040515 - } - """ - .trimIndent() - - private const val SIGNING_KEY_VALIDATION_DEFAULT = false - - private val EXAMPLE_RSA_PRIVATE_KEY = - """ ------BEGIN RSA PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDdadLFj3DqaYtpZ1ik6ejpIIAU -2KhFqygTvR6SSS9RmcFQu/vojHWzQUhm8aqrGYVkDXCHvEcyBPcZUlWBczcDwQ5YF8VktRpMxfAI -K/OZRmfrhK9jAZsxOPCCXMOY+JoCbEqEOpsClbHKbgNBgw4AfsISzuWODa47KucIQad202lUZMQ5 -iBQ9CRcSfSis6HyvCMTY5li/9a+O78FfqIGUE4FHeJpsiay2z2AMEzwBPoURkTaSjjOT25e+GY7k -ntilnVne1ORdOPMnOcd28COex55Z+C4QlOr2UIDaAinTAG/0ozwWxd8OaVJJy3mj3dd3AeD2vBMm -ycnhrM+sccqHAgMBAAECggEARelztZDg2QuhixMoUM5RDkeGWc69d14fZfgpzowQRmZTvZ/V32x2 -f7bl2yeEucjxrxF1Tk67dkZOFa9DM4BDR0qusk8zM2Th3IsFizcBkIzEJIA9dvgbXjP58VfEJSme -S5SRBOaSaoME5APPwGBWy/46XoD4x912/dTCpX9Blwl81i7EO8o3NnYhsCWeVoUJTWBzN95OchZF -ozV4pFgv0tZqTNa7VhJtWHHiKkCpdK7gA9SeVEqEeL1TADAa2ngy3BIRfTgdAct6/4N+ZlVsaIXB -1Gnw2RaOoUbHy1PCA6ygtH5lz65p0JdWGcO5l+JNeYmOIeOdJ3QWbVI+CikPGQKBgQDv/JrTewDt -BK5KBpotFsrJeDFKOkC6A8aNeGliAEgJYvCk7zb8RtKCx7ViaYGYWJYj30oYejYEE+vFT7sgzXfE -JPAEiMw4uKeIrbX8QEIP+R25S8iRr657DkTxOvyhO2oQcC7UkZagvrVyQ17VgjtjxGWbc5bRBk5v -u1ZV9VMZuQKBgQDsL/PsLCX3YRBB5+0rpWoTKKrmFtGh31oue+d37Nd7oxBzb2uyF4Q29+zoy1on -EnHNamjjdR95NZoOjEsIIKDTV1C/bsS7be53m0mwKQfecKIXJ+7VN4UsYZXjCajHCr3NFHiIU8ct -pcKGtg7ga5cERIBtrPAi9Qzi7/o1MxUmPwKBgFuSaMWPZuAJ7DNE56mSy9gqa6xmI/KWpDmxG40Q -jGxAe5CD0thacdMDPzwJBDFMhCW1+wDyCRBvRYSpkr7GiA+pBIjGZh6ynwKxPgK9xjdwGB5vQ14L -yikcXcQqfOFM2YDiPYxQ7Ufy3St3d4VCx0SfWSIC7iZeIKnTsvLjxEzJAoGBAKcLFzou0z9N3+Cs -9pnK6OXZ+ly3QNZ6kF6V9VRlJtXjs0vhPsr7ROBXoq/WutEtg11j6AEPIg5o8adeY+bApN40QADU -h8GD84eWRZyYuF8DTDCSZqFYHhEQh6DGgR8dIrX7x2+ryRAozxbVhloE3g7/n9Fx4Xjn1ZBfZ5fe -pBOjAoGAcw2M22BK3NWOHhJ8EC4p6aUIR96lNcCWE/ij+MWCcRdotLDSDuT1q13C+UTxDZ5PsmDs -N/bhCDRZYZoLYo0/h6v4zKBDaX05nVUTCYux0Fo2HGrj5S0bjmgyRcr8+enA3CTzCHZPWZ7ZeADb -0Mbtt/Q4JyOCgwORgXJVQBHxxIQ= ------END RSA PRIVATE KEY----- - """ - .trimIndent() - private val EXAMPLE_EC_PRIVATE_KEY = - """ ------BEGIN EC PRIVATE KEY----- -MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCDQ+B6qEzr/M2sql4X+09X9YlYt8BKA -HX8Q7/6s4KC3qQ== ------END RSA PRIVATE KEY----- - """ - .trimIndent() - } -} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtEditorHighlighter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtEditorHighlighter.kt new file mode 100644 index 00000000..030623e4 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtEditorHighlighter.kt @@ -0,0 +1,203 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.jwtencoderdecoder + +import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.util.TextRange +import com.intellij.ui.IconManager +import com.intellij.util.Alarm +import com.intellij.util.text.DateFormatUtil +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.capitalize +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Objects +import java.util.StringJoiner +import javax.swing.Icon + +internal class JwtEditorHighlighter( + parentDisposable: Disposable, + private val encodedText: ValueProperty, + private val highlightingAttributes: TextAttributes?, +) { + + private val highlightEncodedAlarm = Alarm(parentDisposable) + private val highlightHeaderAlarm = Alarm(parentDisposable) + private val highlightPayloadAlarm = Alarm(parentDisposable) + + fun refresh( + encodedEditor: AdvancedEditor, + headerEditor: AdvancedEditor, + payloadEditor: AdvancedEditor, + isToolDisposed: Boolean, + ) { + scheduleDotSeparatorHighlight(encodedEditor, isToolDisposed) + scheduleHeaderClaimsHighlight(headerEditor, isToolDisposed) + schedulePayloadClaimsHighlight(payloadEditor, isToolDisposed) + } + + fun scheduleDotSeparatorHighlight(editor: AdvancedEditor, isToolDisposed: Boolean) { + if (!isToolDisposed && !highlightEncodedAlarm.isDisposed) { + highlightEncodedAlarm.cancelAllRequests() + highlightEncodedAlarm.addRequest({ highlightDotSeparatorsNow(editor) }, 100) + } + } + + fun highlightDotSeparatorsNow(editor: AdvancedEditor) { + editor.removeTextRangeHighlighters(ENCODED_DOT_SEPARATOR_GROUP_ID) + val encoded = encodedText.get() + var dotIndex = encoded.indexOf('.') + while (dotIndex != -1) { + editor.highlightTextRange( + TextRange(dotIndex, dotIndex + 1), + ENCODED_DOT_SEPARATOR_HIGHLIGHTER_LAYER, + highlightingAttributes, + ENCODED_DOT_SEPARATOR_GROUP_ID, + ) + dotIndex = encoded.indexOf('.', dotIndex + 1) + } + } + + fun scheduleHeaderClaimsHighlight(editor: AdvancedEditor, isToolDisposed: Boolean) { + if (!isToolDisposed && !highlightHeaderAlarm.isDisposed) { + highlightHeaderAlarm.cancelAllRequests() + highlightHeaderAlarm.addRequest({ highlightClaims(editor) }, 100) + } + } + + fun schedulePayloadClaimsHighlight(editor: AdvancedEditor, isToolDisposed: Boolean) { + if (!isToolDisposed && !highlightPayloadAlarm.isDisposed) { + highlightPayloadAlarm.cancelAllRequests() + highlightPayloadAlarm.addRequest({ highlightClaims(editor) }, 100) + } + } + + private fun highlightClaims(editor: AdvancedEditor) { + editor.removeTextRangeHighlighters(HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID) + + JwtEncoderDecoder.unixTimestampSecondsJsonValueRegex.findAll(editor.text).forEach { + val unixTimestampSecondsMatch = it.groups[1] + if (unixTimestampSecondsMatch != null) { + val textRange = + TextRange(unixTimestampSecondsMatch.range.first, unixTimestampSecondsMatch.range.last + 1) + editor.highlightTextRange( + textRange, + UNIX_TIMESTAMP_HIGHLIGHT_LAYER, + null, + HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID, + ClaimFeatureUnixTimestampGutterIconRenderer( + textRange, + unixTimestampSecondsMatch.value.toLong(), + ), + ) + } + } + + JwtEncoderDecoder.claimRegex + .findAll(editor.text) + .mapNotNull { + val claimMatch = it.groups[1] + if (claimMatch != null) { + val standardClaim = StandardClaim.findByFieldName(claimMatch.value) + if (standardClaim != null) { + return@mapNotNull claimMatch.range to standardClaim + } + } + null + } + .forEach { (claimRange, standardClaim) -> + val textRange = TextRange(claimRange.first, claimRange.last + 1) + editor.highlightTextRange( + textRange, + CLAIM_REGEX_MATCH_HIGHLIGHT_LAYER, + null, + HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID, + StandardClaimGutterIconRenderer(textRange, standardClaim), + ) + } + } + + companion object { + + private const val HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID = "claims" + private const val ENCODED_DOT_SEPARATOR_GROUP_ID = "encodedDotSeparator" + private const val ENCODED_DOT_SEPARATOR_HIGHLIGHTER_LAYER = HighlighterLayer.SELECTION - 1 + private const val UNIX_TIMESTAMP_HIGHLIGHT_LAYER = HighlighterLayer.SELECTION - 1 + private const val CLAIM_REGEX_MATCH_HIGHLIGHT_LAYER = UNIX_TIMESTAMP_HIGHLIGHT_LAYER - 1 + } +} + +internal class StandardClaimGutterIconRenderer( + private val textRange: TextRange, + private val standardClaim: StandardClaim, +) : GutterIconRenderer() { + + override fun getTooltipText(): String = standardClaim.toString() + + override fun getIcon(): Icon = AllIcons.Gutter.JavadocRead + + override fun equals(other: Any?): Boolean { + return if (other != null && other is StandardClaimGutterIconRenderer) { + other.textRange == textRange && other.standardClaim == standardClaim + } else { + false + } + } + + override fun hashCode(): Int = Objects.hash(textRange, standardClaim) + + override fun getAlignment(): Alignment = Alignment.RIGHT +} + +internal class ClaimFeatureUnixTimestampGutterIconRenderer( + private val textRange: TextRange, + private val unixTimestampSeconds: Long, +) : GutterIconRenderer() { + + override fun getTooltipText(): String { + val tooltipText = StringJoiner("

") + + tooltipText.add( + Instant.ofEpochSecond(unixTimestampSeconds) + .atZone(ZoneId.systemDefault()) + .format(DateTimeFormatter.ISO_ZONED_DATE_TIME) + ) + + val diff = + DateFormatUtil.formatBetweenDates( + unixTimestampSeconds.times(1000), + System.currentTimeMillis(), + ) + tooltipText.add("${diff.capitalize()}.") + + return tooltipText.toString() + } + + override fun getIcon(): Icon = clockGutterIcon + + override fun equals(other: Any?): Boolean { + return if (other != null && other is ClaimFeatureUnixTimestampGutterIconRenderer) { + other.textRange == textRange && other.unixTimestampSeconds == unixTimestampSeconds + } else { + false + } + } + + override fun hashCode(): Int = Objects.hash(textRange, unixTimestampSeconds) + + override fun getAlignment(): Alignment = Alignment.LEFT + + companion object { + + private val clockGutterIcon = + IconManager.getInstance() + .getIcon( + "dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter.svg", + ClaimFeatureUnixTimestampGutterIconRenderer::class.java.classLoader, + ) + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtEncoderDecoder.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtEncoderDecoder.kt new file mode 100644 index 00000000..a7ffd676 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtEncoderDecoder.kt @@ -0,0 +1,734 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.jwtencoderdecoder + +import com.intellij.icons.AllIcons +import com.intellij.json.JsonLanguage +import com.intellij.lang.Language +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.fileTypes.PlainTextLanguage +import com.intellij.openapi.observable.properties.AtomicProperty +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.Splitter +import com.intellij.openapi.ui.getUserData +import com.intellij.openapi.ui.putUserData +import com.intellij.ui.JBSplitter +import com.intellij.ui.components.JBTabbedPane +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.LabelPosition +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.actionButton +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.rows +import com.intellij.ui.dsl.builder.selected +import com.intellij.ui.dsl.builder.whenItemSelectedFromUi +import com.intellij.ui.dsl.builder.whenStateChangedFromUi +import com.intellij.ui.dsl.builder.whenTextChangedFromUi +import com.intellij.ui.layout.ComboBoxPredicate +import com.intellij.ui.layout.not +import com.intellij.util.Alarm +import com.intellij.util.ui.JBUI +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.Companion.createSensitiveInputsHandlingToolTipText +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.PropertyComponentPredicate +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.SimpleToggleAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.onSelectionChanged +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.registerDynamicToolTip +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.setValidationResultBorder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.wrapTabbedPaneContent +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import java.util.Base64 +import javax.swing.JComponent + +class JwtEncoderDecoder( + private val context: DeveloperUiToolContext, + private val configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + private val project: Project?, +) : DeveloperUiTool(parentDisposable) { + // -- Properties ---------------------------------------------------------- // + + private var liveConversion = configuration.register("liveConversion", true) + private var encodedText = configuration.register("encodedText", "", INPUT, EXAMPLE_ENCODED) + private var headerText = configuration.register("headerText", "", INPUT, exampleHeader) + private var payloadText = configuration.register("payloadText", "", INPUT, examplePayload) + + private val conversionAlarm by lazy { Alarm(parentDisposable) } + private val validationAlarm by lazy { Alarm(parentDisposable) } + + private var selectedTab = JwtTab.DECODE_ENCODE + private val encodedEditorLabel = AtomicProperty(sharedEncodedEditorLabel(selectedTab)) + private var lastActiveInput: AdvancedEditor? = null + private val encodedEditor by lazy { createEncodedEditor() } + private val headerEditor by lazy { createHeaderEditor() } + private val payloadEditor by lazy { createPayloadEditor() } + + private val highlightingAttributes by lazy { + EditorColorsManager.getInstance() + .globalScheme + .getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES) + } + + private val highlighter by lazy { + JwtEditorHighlighter(parentDisposable, encodedText, highlightingAttributes) + } + private val jwt = Jwt(configuration, encodedText, headerText, payloadText) + private val validation = JwtValidation(configuration) + + // -- Initialization ------------------------------------------------------ // + + init { + liveConversion.afterChange(parentDisposable) { handleLiveConversionSwitch() } + + jwt.signature.secretEncodingMode.afterChangeConsumeEvent(null) { event -> + if (event.valueChanged()) { + convertFromUi(ChangeOrigin.SIGNATURE_CONFIGURATION) + } + } + + validation.secretEncodingMode.afterChangeConsumeEvent(null) { event -> + if (event.valueChanged()) { + validateFromUi() + } + } + } + + // -- Exposed Methods ----------------------------------------------------- // + + override fun Panel.buildUi() { + row { + cell( + JBSplitter(true, 0.2f).apply { + firstComponent = createSharedEncodedComponent() + secondComponent = + JBTabbedPane().apply { + tabComponentInsets = JBUI.emptyInsets() + + addTab( + UiToolsBundle.message("jwt-encoder-decoder.tab.decode-encode"), + createTabContent(JwtTab.DECODE_ENCODE, createEncodingDecodingTab()), + ) + addTab( + UiToolsBundle.message("jwt-encoder-decoder.tab.validate"), + createTabContent(JwtTab.VALIDATE, createValidationTab()), + ) + + onSelectionChanged { selectedComponent -> + selectedComponent.getUserData(jwtTabKey)?.let { handleTabSelectionChanged(it) } + } + } + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + } + + override fun afterBuildUi() { + convertFromUi(ChangeOrigin.ENCODED) + validateFromUi() + } + + override fun reset() { + convert(ChangeOrigin.ENCODED) + validateJwt() + } + + // -- Private Methods ----------------------------------------------------- // + + private fun createSharedEncodedComponent(): JComponent = panel { + row { label("").bindText(encodedEditorLabel).resizableColumn() }.bottomGap(BottomGap.NONE) + row { + cell(encodedEditor.component) + .validationOnApply(encodedEditor.bindValidator(jwt.encodedErrorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + } + + private fun createEncodingDecodingTab(): JComponent = panel { + val signatureErrors = jwt.signatureErrorHolder.asComponentPredicate() + row { + text("") + .bindText(jwt.signatureErrorHolder.asPropertyForTextCell()) + .visibleIf(signatureErrors) + .resizableColumn() + } + .topGap(TopGap.NONE) + row { cell(createEncodingDecodingComponent()).align(Align.FILL).resizableColumn() } + .resizableRow() + .topGap(TopGap.NONE) + } + + private fun createValidationTab(): JComponent = panel { + row { text("").bindText(validation.tokenMetadata).resizableColumn() }.topGap(TopGap.NONE) + row { + text("") + .bindText(validation.validResultMessage) + .visibleIf( + PropertyComponentPredicate(validation.resultState, ValidationResultState.VALID) + ) + .resizableColumn() + text("") + .bindText(validation.invalidResultMessage) + .visibleIf( + PropertyComponentPredicate(validation.resultState, ValidationResultState.INVALID) + ) + .resizableColumn() + } + .topGap(TopGap.NONE) + row { cell(createValidationComponent()).align(Align.FILL).resizableColumn() } + .resizableRow() + .topGap(TopGap.NONE) + } + + @Suppress("UnstableApiUsage") + private fun createEncodingDecodingComponent(): JComponent = panel { + row { + val liveConversionCheckBox = + checkBox(UiToolsBundle.message("converter.live-conversion")) + .bindSelected(liveConversion) + .gap(RightGap.SMALL) + + button(UiToolsBundle.message("jwt-encoder-decoder.button.decode")) { + convert(ChangeOrigin.ENCODED) + } + .enabledIf(liveConversionCheckBox.selected.not()) + .gap(RightGap.SMALL) + button(UiToolsBundle.message("jwt-encoder-decoder.button.encode")) { + convert(ChangeOrigin.SIGNATURE_CONFIGURATION) + } + .enabledIf(liveConversionCheckBox.selected.not()) + } + + if (context.prioritizeVerticalLayout) { + row { + cell( + Splitter(true, 0.3f).apply { + firstComponent = createHeaderEditorComponent() + secondComponent = createPayloadEditorComponent() + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + .bottomGap(BottomGap.NONE) + } else { + row { + cell( + Splitter(false, 0.5f).apply { + firstComponent = createHeaderEditorComponent() + secondComponent = createPayloadEditorComponent() + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + .bottomGap(BottomGap.NONE) + } + + collapsibleGroup(UiToolsBundle.message("jwt-encoder-decoder.signature-configuration.title")) { + lateinit var signatureAlgorithmComboBox: ComboBox + row { + signatureAlgorithmComboBox = + comboBox(SignatureAlgorithm.entries) + .label( + UiToolsBundle.message("jwt-encoder-decoder.signature-configuration.algorithm") + ) + .bindItem(jwt.signature.algorithm) + .whenItemSelectedFromUi { convertFromUi(ChangeOrigin.SIGNATURE_CONFIGURATION) } + .component + } + .layout(RowLayout.PARENT_GRID) + .topGap(TopGap.NONE) + + row { + label(UiToolsBundle.message("jwt-encoder-decoder.signature-configuration.secret-key")) + expandableTextField() + .align(AlignX.FILL) + .bindText(jwt.signature.secret) + .whenTextChangedFromUi { convertFromUi(ChangeOrigin.SIGNATURE_CONFIGURATION) } + .gap(RightGap.SMALL) + .resizableColumn() + .registerDynamicToolTip { generalSettings.createSensitiveInputsHandlingToolTipText() } + + val encodingActions = + mutableListOf().apply { + SecretKeyEncodingMode.entries.forEach { secretKeyEncodingModeValue -> + add( + SimpleToggleAction( + text = secretKeyEncodingModeValue.title, + icon = AllIcons.Actions.ToggleSoftWrap, + isSelected = { + jwt.signature.secretEncodingMode.get() == secretKeyEncodingModeValue + }, + setSelected = { + jwt.signature.secretEncodingMode.set(secretKeyEncodingModeValue) + }, + ) + ) + } + } + actionButton( + UiUtils.actionsPopup( + title = + UiToolsBundle.message("jwt-encoder-decoder.signature-configuration.encoding"), + icon = AllIcons.General.Settings, + actions = encodingActions, + ) + ) + } + .visibleIf( + ComboBoxPredicate(signatureAlgorithmComboBox) { + it?.kind == SignatureAlgorithmKind.HMAC + } + ) + .layout(RowLayout.PARENT_GRID) + + row { + textArea() + .rows(5) + .align(Align.FILL) + .label( + label = + UiToolsBundle.message("jwt-encoder-decoder.signature-configuration.private-key"), + position = LabelPosition.TOP, + ) + .bindText(jwt.signature.privateKey) + .setValidationResultBorder() + .whenTextChangedFromUi { convertFromUi(ChangeOrigin.SIGNATURE_CONFIGURATION) } + .validationInfo(jwt.signature.privateKeyErrorHolder.asValidation()) + .registerDynamicToolTip { generalSettings.createSensitiveInputsHandlingToolTipText() } + } + .visibleIf(ComboBoxPredicate(signatureAlgorithmComboBox) { it?.kind?.keyFactory != null }) + + row { + checkBox(UiToolsBundle.message("jwt-encoder-decoder.strict-key-validation")) + .bindSelected(jwt.signature.strictSigningKeyValidation) + .whenStateChangedFromUi { convertFromUi(ChangeOrigin.SIGNATURE_CONFIGURATION) } + .gap(RightGap.SMALL) + contextHelp(UiToolsBundle.message("jwt-encoder-decoder.strict-key-validation.help")) + } + .visibleIf( + ComboBoxPredicate(signatureAlgorithmComboBox) { + it?.kind != SignatureAlgorithmKind.NONE + } + ) + } + .apply { expanded = false } + .topGap(TopGap.NONE) + } + + @Suppress("UnstableApiUsage") + private fun createValidationComponent(): JComponent = panel { + lateinit var validationSourceComboBox: ComboBox + row { + validationSourceComboBox = + comboBox(ValidationKeySource.entries) + .label(UiToolsBundle.message("jwt-encoder-decoder.validation.key-source")) + .bindItem(validation.keySource) + .whenItemSelectedFromUi { validateFromUi() } + .component + } + .layout(RowLayout.PARENT_GRID) + .topGap(TopGap.NONE) + + row { + label(UiToolsBundle.message("jwt-encoder-decoder.signature-configuration.secret-key")) + expandableTextField() + .align(AlignX.FILL) + .bindText(validation.secret) + .whenTextChangedFromUi { validateFromUi() } + .gap(RightGap.SMALL) + .resizableColumn() + .registerDynamicToolTip { generalSettings.createSensitiveInputsHandlingToolTipText() } + + val encodingActions = + mutableListOf().apply { + SecretKeyEncodingMode.entries.forEach { secretKeyEncodingModeValue -> + add( + SimpleToggleAction( + text = secretKeyEncodingModeValue.title, + icon = AllIcons.Actions.ToggleSoftWrap, + isSelected = { + validation.secretEncodingMode.get() == secretKeyEncodingModeValue + }, + setSelected = { validation.secretEncodingMode.set(secretKeyEncodingModeValue) }, + ) + ) + } + } + actionButton( + UiUtils.actionsPopup( + title = UiToolsBundle.message("jwt-encoder-decoder.signature-configuration.encoding"), + icon = AllIcons.General.Settings, + actions = encodingActions, + ) + ) + } + .visibleIf(ComboBoxPredicate(validationSourceComboBox) { it == ValidationKeySource.SECRET }) + .layout(RowLayout.PARENT_GRID) + + row { + textArea() + .rows(5) + .align(Align.FILL) + .label( + label = UiToolsBundle.message("jwt-encoder-decoder.validation.public-key-or-jwk"), + position = LabelPosition.TOP, + ) + .bindText(validation.publicKey) + .setValidationResultBorder() + .whenTextChangedFromUi { validateFromUi() } + .registerDynamicToolTip { generalSettings.createSensitiveInputsHandlingToolTipText() } + } + .visibleIf( + ComboBoxPredicate(validationSourceComboBox) { it == ValidationKeySource.PUBLIC_KEY } + ) + + row { + val jwksFetchEnabled = PropertyComponentPredicate(validation.fetchingJwks, false) + + textField() + .align(Align.FILL) + .label(UiToolsBundle.message("jwt-encoder-decoder.validation.jwks-url")) + .bindText(validation.jwksUrl) + .whenTextChangedFromUi { validateFromUi() } + .gap(RightGap.SMALL) + .resizableColumn() + .enabledIf(jwksFetchEnabled) + button(UiToolsBundle.message("jwt-encoder-decoder.validation.fetch")) { fetchJwks() } + .enabledIf(jwksFetchEnabled) + } + .visibleIf(ComboBoxPredicate(validationSourceComboBox) { it == ValidationKeySource.JWKS }) + .layout(RowLayout.PARENT_GRID) + + row { + textArea() + .rows(8) + .align(Align.FILL) + .label( + label = UiToolsBundle.message("jwt-encoder-decoder.validation.jwks-json"), + position = LabelPosition.TOP, + ) + .bindText(validation.jwksJson) + .setValidationResultBorder() + .whenTextChangedFromUi { validateFromUi() } + } + .visibleIf(ComboBoxPredicate(validationSourceComboBox) { it == ValidationKeySource.JWKS }) + + row { + checkBox(UiToolsBundle.message("jwt-encoder-decoder.strict-key-validation")) + .bindSelected(validation.strictKeyValidation) + .whenStateChangedFromUi { validateFromUi() } + .gap(RightGap.SMALL) + contextHelp(UiToolsBundle.message("jwt-encoder-decoder.strict-key-validation.help")) + } + .topGap(TopGap.NONE) + } + + private fun createPayloadEditorComponent(): JComponent = panel { + row { + cell(payloadEditor.component) + .validationOnApply(payloadEditor.bindValidator(jwt.payloadErrorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + } + + private fun createHeaderEditorComponent(): JComponent = panel { + row { + cell(headerEditor.component) + .validationOnApply(headerEditor.bindValidator(jwt.headerErrorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + } + + private fun convertFromUi(changeOrigin: ChangeOrigin) { + if (!liveConversion.get()) { + return + } + + convert(changeOrigin) + } + + private fun validateFromUi() { + if (configuration.isResetting) { + return + } + + if (!isDisposed && !validationAlarm.isDisposed) { + validationAlarm.cancelAllRequests() + validationAlarm.addRequest({ validateJwt() }, 100) + } + } + + private fun convert(changeOrigin: ChangeOrigin) { + if (configuration.isResetting) { + return + } + + if (!isDisposed && !conversionAlarm.isDisposed) { + conversionAlarm.cancelAllRequests() + conversionAlarm.addRequest({ doConvert(changeOrigin) }, 100) + } + } + + private fun doConvert(changeOrigin: ChangeOrigin) { + when (changeOrigin) { + ChangeOrigin.ENCODED -> jwt.decodeJwt() + ChangeOrigin.HEADER_OR_PAYLOAD -> jwt.encodeJwt() + ChangeOrigin.SIGNATURE_CONFIGURATION -> { + jwt.setAlgorithmInHeader() + jwt.encodeJwt() + } + } + + validation.validate(encodedText.get()) + refreshHighlights() + validate() + } + + private fun validateJwt() { + jwt.decodeJwt() + validation.validate(encodedText.get()) + refreshHighlights() + validate() + } + + private fun refreshHighlights() { + highlighter.refresh( + encodedEditor = encodedEditor, + headerEditor = headerEditor, + payloadEditor = payloadEditor, + isToolDisposed = isDisposed, + ) + } + + private fun createEncodedEditor(): AdvancedEditor = + createEditor( + id = "encoded", + changeOrigin = ChangeOrigin.ENCODED, + title = null, + language = PlainTextLanguage.INSTANCE, + textProperty = encodedText, + ) { + if (selectedTab == JwtTab.VALIDATE && !liveConversion.get()) { + validateFromUi() + } + highlighter.scheduleDotSeparatorHighlight(encodedEditor, isDisposed) + } + + private fun createHeaderEditor(): AdvancedEditor = + createEditor( + id = "header", + changeOrigin = ChangeOrigin.HEADER_OR_PAYLOAD, + title = UiToolsBundle.message("jwt-encoder-decoder.editor.header"), + language = JsonLanguage.INSTANCE, + textProperty = headerText, + ) { + highlighter.scheduleHeaderClaimsHighlight(headerEditor, isDisposed) + } + + private fun createPayloadEditor(): AdvancedEditor = + createEditor( + id = "payload", + changeOrigin = ChangeOrigin.HEADER_OR_PAYLOAD, + title = UiToolsBundle.message("jwt-encoder-decoder.editor.payload"), + language = JsonLanguage.INSTANCE, + textProperty = payloadText, + ) { + highlighter.schedulePayloadClaimsHighlight(payloadEditor, isDisposed) + } + + private fun createEditor( + id: String, + changeOrigin: ChangeOrigin, + title: String?, + language: Language, + textProperty: ValueProperty, + onTextChangeFromUi: (() -> Unit)? = null, + ) = + AdvancedEditor( + id = id, + context = context, + configuration = configuration, + project = project, + title = title, + editorMode = EditorMode.INPUT_OUTPUT, + parentDisposable = parentDisposable, + textProperty = textProperty, + initialLanguage = language, + ) + .apply { + onFocusGained { lastActiveInput = this } + this.onTextChangeFromUi { _ -> + lastActiveInput = this + convertFromUi(changeOrigin) + onTextChangeFromUi?.invoke() + } + } + + private fun handleLiveConversionSwitch() { + if (liveConversion.get()) { + when (lastActiveInput) { + encodedEditor -> convert(ChangeOrigin.ENCODED) + headerEditor, + payloadEditor -> convert(ChangeOrigin.SIGNATURE_CONFIGURATION) + + null -> {} + } + } + } + + private fun createTabContent(jwtTab: JwtTab, component: JComponent): JComponent = + component.apply { putUserData(jwtTabKey, jwtTab) }.wrapTabbedPaneContent() + + private fun handleTabSelectionChanged(jwtTab: JwtTab) { + selectedTab = jwtTab + encodedEditorLabel.set(sharedEncodedEditorLabel(jwtTab)) + if (jwtTab == JwtTab.VALIDATE) { + validateFromUi() + } + } + + private fun sharedEncodedEditorLabel(jwtTab: JwtTab): String = + when (jwtTab) { + JwtTab.DECODE_ENCODE -> UiToolsBundle.message("jwt-encoder-decoder.editor.jwt-input-output") + JwtTab.VALIDATE -> UiToolsBundle.message("jwt-encoder-decoder.editor.jwt-input") + } + + private fun fetchJwks() { + validation.fetchJwks(project) { validateJwt() } + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("jwt-encoder-decoder.menu-title"), + contentTitle = UiToolsBundle.message("jwt-encoder-decoder.content-title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> JwtEncoderDecoder) = { configuration -> + JwtEncoderDecoder(context, configuration, parentDisposable, project) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + private val jwtTabKey = com.intellij.openapi.util.Key.create("jwtTab") + + internal val urlEncoder: Base64.Encoder = Base64.getUrlEncoder().withoutPadding() + + internal val unixTimestampSecondsJsonValueRegex = + Regex(":\\s*(?\\b\\d{1,10}\\b)") + internal val claimRegex = Regex("\\s?\"(?[a-zA-Z]+)\"\\s?:") + internal val rawKeyRegex = Regex("\\r?\\n|\\r|\\s?-+(BEGIN|END).*KEY-+\\s?") + + internal val defaultSignatureAlgorithm = SignatureAlgorithm.HMAC256 + internal const val SIGNING_KEY_VALIDATION_DEFAULT = false + + internal const val EXAMPLE_ENCODED = + "ewogICJ0eXAiOiJKV1QiLAogICJhbGciOiJIUzI1NiIKfQ.ewogICJqdGkiOiI5NjQ5MmQ1OS0wYWQ1LTRjMDAtODkyZC01OTBhZDVhYzAwZjMiLAogICJzdWIiOiIwMTIzNDU2Nzg5IiwKICAibmFtZSI6IkpvaG4gRG9lIiwKICAiaWF0IjoxNjgxMDQwNTE1Cn0.IqeNl3lHSUfPfEYmttvlQp1sH9LpAoPJlUiSv4XPDSE" + internal const val EXAMPLE_SECRET = "s3cre!" + internal val exampleHeader = + """ + { + "typ":"JWT", + "alg":"HS256" + } + """ + .trimIndent() + internal val examplePayload = + """ + { + "jti":"96492d59-0ad5-4c00-892d-590ad5ac00f3", + "sub":"0123456789", + "name":"John Doe", + "iat":1681040515 + } + """ + .trimIndent() + + internal val exampleRsaPrivateKey = + """ + -----BEGIN RSA PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDdadLFj3DqaYtpZ1ik6ejpIIAU + 2KhFqygTvR6SSS9RmcFQu/vojHWzQUhm8aqrGYVkDXCHvEcyBPcZUlWBczcDwQ5YF8VktRpMxfAI + K/OZRmfrhK9jAZsxOPCCXMOY+JoCbEqEOpsClbHKbgNBgw4AfsISzuWODa47KucIQad202lUZMQ5 + iBQ9CRcSfSis6HyvCMTY5li/9a+O78FfqIGUE4FHeJpsiay2z2AMEzwBPoURkTaSjjOT25e+GY7k + ntilnVne1ORdOPMnOcd28COex55Z+C4QlOr2UIDaAinTAG/0ozwWxd8OaVJJy3mj3dd3AeD2vBMm + ycnhrM+sccqHAgMBAAECggEARelztZDg2QuhixMoUM5RDkeGWc69d14fZfgpzowQRmZTvZ/V32x2 + f7bl2yeEucjxrxF1Tk67dkZOFa9DM4BDR0qusk8zM2Th3IsFizcBkIzEJIA9dvgbXjP58VfEJSme + S5SRBOaSaoME5APPwGBWy/46XoD4x912/dTCpX9Blwl81i7EO8o3NnYhsCWeVoUJTWBzN95OchZF + ozV4pFgv0tZqTNa7VhJtWHHiKkCpdK7gA9SeVEqEeL1TADAa2ngy3BIRfTgdAct6/4N+ZlVsaIXB + 1Gnw2RaOoUbHy1PCA6ygtH5lz65p0JdWGcO5l+JNeYmOIeOdJ3QWbVI+CikPGQKBgQDv/JrTewDt + BK5KBpotFsrJeDFKOkC6A8aNeGliAEgJYvCk7zb8RtKCx7ViaYGYWJYj30oYejYEE+vFT7sgzXfE + JPAEiMw4uKeIrbX8QEIP+R25S8iRr657DkTxOvyhO2oQcC7UkZagvrVyQ17VgjtjxGWbc5bRBk5v + u1ZV9VMZuQKBgQDsL/PsLCX3YRBB5+0rpWoTKKrmFtGh31oue+d37Nd7oxBzb2uyF4Q29+zoy1on + EnHNamjjdR95NZoOjEsIIKDTV1C/bsS7be53m0mwKQfecKIXJ+7VN4UsYZXjCajHCr3NFHiIU8ct + pcKGtg7ga5cERIBtrPAi9Qzi7/o1MxUmPwKBgFuSaMWPZuAJ7DNE56mSy9gqa6xmI/KWpDmxG40Q + jGxAe5CD0thacdMDPzwJBDFMhCW1+wDyCRBvRYSpkr7GiA+pBIjGZh6ynwKxPgK9xjdwGB5vQ14L + yikcXcQqfOFM2YDiPYxQ7Ufy3St3d4VCx0SfWSIC7iZeIKnTsvLjxEzJAoGBAKcLFzou0z9N3+Cs + 9pnK6OXZ+ly3QNZ6kF6V9VRlJtXjs0vhPsr7ROBXoq/WutEtg11j6AEPIg5o8adeY+bApN40QADU + h8GD84eWRZyYuF8DTDCSZqFYHhEQh6DGgR8dIrX7x2+ryRAozxbVhloE3g7/n9Fx4Xjn1ZBfZ5fe + pBOjAoGAcw2M22BK3NWOHhJ8EC4p6aUIR96lNcCWE/ij+MWCcRdotLDSDuT1q13C+UTxDZ5PsmDs + N/bhCDRZYZoLYo0/h6v4zKBDaX05nVUTCYux0Fo2HGrj5S0bjmgyRcr8+enA3CTzCHZPWZ7ZeADb + 0Mbtt/Q4JyOCgwORgXJVQBHxxIQ= + -----END RSA PRIVATE KEY----- + """ + .trimIndent() + + internal val exampleEcPrivateKey = + """ + -----BEGIN EC PRIVATE KEY----- + MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCDQ+B6qEzr/M2sql4X+09X9YlYt8BKA + HX8Q7/6s4KC3qQ== + -----END RSA PRIVATE KEY----- + """ + .trimIndent() + } + + private enum class JwtTab { + DECODE_ENCODE, + VALIDATE, + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtEncodingModel.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtEncodingModel.kt new file mode 100644 index 00000000..9e8c314a --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtEncodingModel.kt @@ -0,0 +1,496 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.jwtencoderdecoder + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.intellij.util.ExceptionUtil +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.decodeBase64String +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.SENSITIVE +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.ObjectMapperService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import java.security.Key +import java.security.KeyFactory +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 +import org.apache.commons.codec.binary.Base32 +import org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256 +import org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384 +import org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512 +import org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA256 +import org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA384 +import org.jose4j.jws.AlgorithmIdentifiers.HMAC_SHA512 +import org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA256 +import org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA384 +import org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA512 +import org.jose4j.jws.JsonWebSignature +import org.jose4j.keys.HmacKey + +internal enum class ChangeOrigin { + + ENCODED, + HEADER_OR_PAYLOAD, + SIGNATURE_CONFIGURATION, +} + +internal enum class SignatureAlgorithmKind(val keyFactory: KeyFactory?) { + + NONE(null), + HMAC(null), + RSA(KeyFactory.getInstance("RSA")), + ECDSA(KeyFactory.getInstance("EC")), +} + +internal enum class SignatureAlgorithm( + val jwtHeaderValue: String, + val kind: SignatureAlgorithmKind, + @Suppress("unused") // May be used for JWK validation + val algorithmIdentifiers: String, +) { + + NONE("none", SignatureAlgorithmKind.NONE, "none"), + HMAC256("HS256", SignatureAlgorithmKind.HMAC, HMAC_SHA256), + HMAC384("HS384", SignatureAlgorithmKind.HMAC, HMAC_SHA384), + HMAC512("HS512", SignatureAlgorithmKind.HMAC, HMAC_SHA512), + RSA256("RS256", SignatureAlgorithmKind.RSA, RSA_USING_SHA256), + RSA384("RS384", SignatureAlgorithmKind.RSA, RSA_USING_SHA384), + RSA512("RS512", SignatureAlgorithmKind.RSA, RSA_USING_SHA512), + ECDSA256("ES256", SignatureAlgorithmKind.ECDSA, ECDSA_USING_P256_CURVE_AND_SHA256), + ECDSA384("ES384", SignatureAlgorithmKind.ECDSA, ECDSA_USING_P384_CURVE_AND_SHA384), + ECDSA512("ES512", SignatureAlgorithmKind.ECDSA, ECDSA_USING_P521_CURVE_AND_SHA512); + + override fun toString(): String = + if (this == NONE) { + UiToolsBundle.message("jwt-encoder-decoder.signature-algorithm.none") + } else { + UiToolsBundle.message("jwt-encoder-decoder.signature-algorithm.named", name, jwtHeaderValue) + } + + companion object { + + fun findByJwtHeaderValue(jwtHeaderValue: String): SignatureAlgorithm? = + entries.firstOrNull { it.jwtHeaderValue == jwtHeaderValue } + } +} + +internal enum class SecretKeyEncodingMode(val title: String) { + + RAW(UiToolsBundle.message("jwt-encoder-decoder.secret-key-encoding-mode.raw")), + BASE32(UiToolsBundle.message("jwt-encoder-decoder.secret-key-encoding-mode.base32")), + BASE64(UiToolsBundle.message("jwt-encoder-decoder.secret-key-encoding-mode.base64")), +} + +internal enum class ValidationKeySource(private val title: String) { + + SECRET(UiToolsBundle.message("jwt-encoder-decoder.validation-key-source.secret")), + PUBLIC_KEY(UiToolsBundle.message("jwt-encoder-decoder.validation-key-source.public-key")), + JWKS(UiToolsBundle.message("jwt-encoder-decoder.validation-key-source.jwks")); + + override fun toString(): String = title +} + +internal enum class ValidationResultState { + + NONE, + VALID, + INVALID, +} + +internal enum class StandardClaim( + val fieldName: String, + val title: String, + val description: String, +) { + + TYP( + fieldName = "typ", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.typ.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.typ.description"), + ), + ISSUER( + fieldName = "iss", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.iss.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.iss.description"), + ), + SUBJECT( + fieldName = "sub", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.sub.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.sub.description"), + ), + AUDIENCE( + fieldName = "aud", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.aud.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.aud.description"), + ), + EXPIRATION_TIME( + fieldName = "exp", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.exp.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.exp.description"), + ), + NOT_BEFORE_TIME( + fieldName = "nbf", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.nbf.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.nbf.description"), + ), + ISSUED_AT_TIME( + fieldName = "iat", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.iat.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.iat.description"), + ), + JWT_ID( + fieldName = "jti", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.jti.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.jti.description"), + ), + ALG( + fieldName = "alg", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.alg.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.alg.description"), + ), + AZP( + fieldName = "azp", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.azp.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.azp.description"), + ), + SID( + fieldName = "sid", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.sid.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.sid.description"), + ), + NONCE( + fieldName = "nonce", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.nonce.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.nonce.description"), + ), + AT_HASH( + fieldName = "at_hash", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.at-hash.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.at-hash.description"), + ), + C_HASH( + fieldName = "c_hash", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.c-hash.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.c-hash.description"), + ), + ACT( + fieldName = "act", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.act.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.act.description"), + ), + AUTH_TIME( + fieldName = "auth_time", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.auth-time.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.auth-time.description"), + ), + SCOPE( + fieldName = "scope", + title = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.scope.title"), + description = UiToolsBundle.message("jwt-encoder-decoder.standard-claim.scope.description"), + ); + + override fun toString(): String = + UiToolsBundle.message( + "jwt-encoder-decoder.standard-claim.tooltip", + title, + fieldName, + description, + ) + + companion object { + + fun findByFieldName(fieldName: String): StandardClaim? = + entries.firstOrNull { it.fieldName == fieldName } + } +} + +internal class Jwt( + configuration: DeveloperToolConfiguration, + val encoded: ValueProperty, + val header: ValueProperty, + val payload: ValueProperty, +) { + + val encodedErrorHolder = ErrorHolder() + val headerErrorHolder = ErrorHolder() + val payloadErrorHolder = ErrorHolder() + val signatureErrorHolder = + ErrorHolder(addErrorIconToMessage = true, surroundMessageWithHtml = false) + + val signature = Signature(configuration, signatureErrorHolder) + + fun decodeJwt() { + clearErrorHolders() + + val jwtParts = encoded.get().split('.', limit = 3) + val numOfJwtParts = jwtParts.size + + if (numOfJwtParts >= 1) { + val handleError: (Exception) -> Unit = { error -> + header.set(jwtParts[0]) + headerErrorHolder.add(error) + } + parseAsJson(text = jwtParts[0], textIsBase64 = true, handleError) { + parseHeader(it) + header.set(ObjectMapperService.instance.prettyPrintJson(it)) + } + } else { + header.set("") + } + + if (numOfJwtParts >= 2) { + val handleError: (Exception) -> Unit = { error -> + payload.set(jwtParts[1]) + payloadErrorHolder.add(error) + } + parseAsJson(text = jwtParts[1], textIsBase64 = true, handleError) { + payload.set(ObjectMapperService.instance.prettyPrintJson(it)) + } + } else { + payload.set("") + } + } + + fun encodeJwt() { + clearErrorHolders() + + val jsonMapper = ObjectMapperService.instance.jsonMapper() + + val headerJson = + try { + val headerJson = jsonMapper.readTree(header.get()) + parseHeader(headerJson) + headerJson + } catch (e: Exception) { + headerErrorHolder.add(e) + null + } + + val payloadJson = + try { + jsonMapper.readTree(payload.get()) + } catch (e: Exception) { + payloadErrorHolder.add(e) + null + } + + if (headerErrorHolder.isSet() || payloadErrorHolder.isSet()) { + encoded.set("") + signatureErrorHolder.add( + UiToolsBundle.message( + "jwt-encoder-decoder.encode.signature-unavailable.header-payload-errors" + ) + ) + } else { + val encodedHeader = + JwtEncoderDecoder.urlEncoder + .encode(jsonMapper.writeValueAsString(headerJson!!).encodeToByteArray()) + .decodeToString() + val encodedPayload = + JwtEncoderDecoder.urlEncoder + .encode(jsonMapper.writeValueAsString(payloadJson!!).encodeToByteArray()) + .decodeToString() + val encodedSignature = signature.compute(encodedHeader, encodedPayload) + if (encodedSignature == null) { + encoded.set("") + signatureErrorHolder.addIfNoErrors( + UiToolsBundle.message( + "jwt-encoder-decoder.encode.signature-unavailable.configuration-errors" + ) + ) + } else { + encoded.set("${encodedHeader}.${encodedPayload}.$encodedSignature") + } + } + } + + fun setAlgorithmInHeader() { + parseAsJson(text = header.get(), textIsBase64 = false, { headerErrorHolder.add(it) }) { + headerNode -> + if (headerNode is ObjectNode) { + headerNode.put("alg", signature.algorithm.get().jwtHeaderValue) + header.set(ObjectMapperService.instance.prettyPrintJson(headerNode)) + } + } + } + + private fun clearErrorHolders() { + encodedErrorHolder.clear() + headerErrorHolder.clear() + payloadErrorHolder.clear() + signatureErrorHolder.clear() + } + + private fun parseHeader(headerNode: JsonNode) { + if (headerNode.has("alg")) { + val algFieldValue = headerNode.get("alg").asText() + val algorithm = SignatureAlgorithm.findByJwtHeaderValue(algFieldValue) + if (algorithm != null) { + signature.algorithm.set(algorithm) + } else { + headerErrorHolder.add( + UiToolsBundle.message("jwt-encoder-decoder.header.unsupported-algorithm", algFieldValue) + ) + } + } else { + headerErrorHolder.add(UiToolsBundle.message("jwt-encoder-decoder.header.missing-algorithm")) + } + } + + private fun parseAsJson( + text: String, + textIsBase64: Boolean, + handleError: (Exception) -> Unit, + handleResult: (JsonNode) -> Unit, + ) { + try { + val actualText = if (textIsBase64) text.decodeBase64String() else text + val jsonNode = ObjectMapperService.instance.jsonMapper().readTree(actualText) + handleResult(jsonNode) + } catch (e: Exception) { + handleError(e) + } + } +} + +internal class Signature( + configuration: DeveloperToolConfiguration, + private val signatureErrorHolder: ErrorHolder, +) { + + val algorithm = configuration.register("algorithm", JwtEncoderDecoder.defaultSignatureAlgorithm) + val strictSigningKeyValidation = + configuration.register( + "signingKeyValidation", + JwtEncoderDecoder.SIGNING_KEY_VALIDATION_DEFAULT, + CONFIGURATION, + ) + val secret = configuration.register("secret", "", SENSITIVE, JwtEncoderDecoder.EXAMPLE_SECRET) + val privateKey = + configuration.registerWithExampleProvider("privateKey", "", SENSITIVE) { + if (algorithm.get().kind == SignatureAlgorithmKind.RSA) { + JwtEncoderDecoder.exampleRsaPrivateKey + } else { + JwtEncoderDecoder.exampleEcPrivateKey + } + } + val secretEncodingMode = + configuration.register("secretKeyEncodingMode", SecretKeyEncodingMode.RAW, CONFIGURATION) + + val privateKeyErrorHolder = ErrorHolder() + + init { + handleAlgorithmChange() + algorithm.afterChangeConsumeEvent(null) { event -> + if (event.valueChanged()) { + handleAlgorithmChange() + } + } + } + + fun compute(encodedHeader: String, encodedPayload: String): String? { + privateKeyErrorHolder.clear() + + return try { + if (algorithm.get() == SignatureAlgorithm.NONE) { + return "" + } + val signingKey = createSigningKey() ?: return null + ExtendedJsonWebSignature() + .apply { + setEncodedHeader(encodedHeader) + setEncodedPayload(encodedPayload) + setKey(signingKey) + isDoKeyValidation = strictSigningKeyValidation.get() + sign() + } + .encodedSignature + } catch (e: Exception) { + signatureErrorHolder.add( + UiToolsBundle.message("jwt-encoder-decoder.signature.compute-failed"), + ExceptionUtil.getRootCause(e), + ) + null + } + } + + private fun createSigningKey(): Key? { + val signatureAlgorithm = algorithm.get() + return when (signatureAlgorithm.kind) { + SignatureAlgorithmKind.NONE -> null + SignatureAlgorithmKind.HMAC -> + HmacKey( + when (secretEncodingMode.get()) { + SecretKeyEncodingMode.RAW -> secret.get().encodeToByteArray() + SecretKeyEncodingMode.BASE32 -> Base32().decode(secret.get()) + SecretKeyEncodingMode.BASE64 -> Base64.getDecoder().decode(secret.get()) + } + ) + + SignatureAlgorithmKind.RSA, + SignatureAlgorithmKind.ECDSA -> + readPrivateKey(signatureAlgorithm.kind.keyFactory!!) ?: return null + } + } + + private fun readPrivateKey(keyFactory: KeyFactory) = + try { + val privateKeyValue = privateKey.get() + if (privateKeyValue.isBlank()) { + privateKeyErrorHolder.add( + UiToolsBundle.message("jwt-encoder-decoder.signature.private-key-required") + ) + null + } else { + keyFactory.generatePrivate(PKCS8EncodedKeySpec(toRawKey(privateKey.get()))) + } + } catch (e: Exception) { + privateKeyErrorHolder.add(e) + null + } + + private fun toRawKey(keyInput: String): ByteArray = + Base64.getDecoder().decode(keyInput.replace(JwtEncoderDecoder.rawKeyRegex, "")) + + private fun handleAlgorithmChange() { + if (generalSettings.loadExamples.get()) { + loadExampleSecrets() + } + } + + private fun loadExampleSecrets() { + val privateKeyValue = privateKey.get() + when (algorithm.get().kind) { + SignatureAlgorithmKind.NONE -> {} + SignatureAlgorithmKind.HMAC -> { + if (secret.get().isBlank()) { + secret.set(JwtEncoderDecoder.EXAMPLE_SECRET) + } + } + + SignatureAlgorithmKind.RSA -> { + if (privateKeyValue.isBlank() || privateKeyValue == JwtEncoderDecoder.exampleEcPrivateKey) { + privateKey.set(JwtEncoderDecoder.exampleRsaPrivateKey) + } + } + + SignatureAlgorithmKind.ECDSA -> { + if ( + privateKeyValue.isBlank() || privateKeyValue == JwtEncoderDecoder.exampleRsaPrivateKey + ) { + privateKey.set(JwtEncoderDecoder.exampleEcPrivateKey) + } + } + } + } +} + +internal class ExtendedJsonWebSignature : JsonWebSignature() { + + @Suppress("RedundantVisibilityModifier") + public override fun setEncodedHeader(encodedHeader: String?) { + super.setEncodedHeader(encodedHeader) + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtValidation.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtValidation.kt new file mode 100644 index 00000000..789503d5 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/jwtencoderdecoder/JwtValidation.kt @@ -0,0 +1,439 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.jwtencoderdecoder + +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.util.ExceptionUtil +import dev.turingcomplete.intellijdevelopertoolsplugin.common.OkHttpClientUtils.applyIntelliJProxySettings +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.decodeBase64String +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.SENSITIVE +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.ObjectMapperService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import java.security.Key +import java.security.spec.X509EncodedKeySpec +import java.util.Base64 +import okhttp3.OkHttpClient +import okhttp3.Request +import org.apache.commons.codec.binary.Base32 +import org.jose4j.jwk.JsonWebKey +import org.jose4j.jwk.JsonWebKeySet +import org.jose4j.jws.JsonWebSignature +import org.jose4j.keys.HmacKey + +internal class JwtValidation(configuration: DeveloperToolConfiguration) { + + private val keySourceName = + configuration.register("validationKeySource", ValidationKeySource.SECRET.name, CONFIGURATION) + val keySource = + ValueProperty( + runCatching { ValidationKeySource.valueOf(keySourceName.get()) } + .getOrDefault(ValidationKeySource.SECRET) + ) + val secret = + configuration.register("validationSecret", "", SENSITIVE, JwtEncoderDecoder.EXAMPLE_SECRET) + val publicKey = configuration.register("validationPublicKey", "", SENSITIVE) + val jwksUrl = configuration.register("validationJwksUrl", "", INPUT) + val jwksJson = configuration.register("validationJwksJson", "", INPUT) + val secretEncodingMode = + configuration.register( + "validationSecretKeyEncodingMode", + SecretKeyEncodingMode.RAW, + CONFIGURATION, + ) + val strictKeyValidation = + configuration.register( + "validationStrictKeyValidation", + JwtEncoderDecoder.SIGNING_KEY_VALIDATION_DEFAULT, + CONFIGURATION, + ) + + val tokenMetadata = + ValueProperty(UiToolsBundle.message("jwt-encoder-decoder.validation.token-metadata.default")) + val resultState = ValueProperty(ValidationResultState.NONE) + val validResultMessage = ValueProperty("") + val invalidResultMessage = ValueProperty("") + val fetchingJwks = ValueProperty(false) + + init { + keySource.afterChangeConsumeEvent(null) { event -> + if (event.valueChanged()) { + keySourceName.set(event.newValue.name) + } + } + } + + fun fetchJwks(project: Project?, onFetchSuccess: () -> Unit = {}) { + val url = jwksUrl.get().trim() + if (url.isBlank()) { + setInvalidResult(UiToolsBundle.message("jwt-encoder-decoder.validation.fetch-jwks.enter-url")) + return + } + if (fetchingJwks.get()) { + return + } + + fetchingJwks.set(true) + + object : + Task.Backgroundable( + project, + UiToolsBundle.message("jwt-encoder-decoder.validation.fetch-jwks.in-progress-title"), + true, + ) { + private lateinit var fetchedJwks: String + + override fun run(indicator: ProgressIndicator) { + indicator.text = + UiToolsBundle.message("jwt-encoder-decoder.validation.fetch-jwks.in-progress") + + val httpClient = OkHttpClient.Builder().applyIntelliJProxySettings(url).build() + val request = Request.Builder().url(url).build() + httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + val statusMessage = response.message.ifBlank { response.code.toString() } + error("HTTP ${response.code}: $statusMessage") + } + fetchedJwks = response.body.string() + } + } + + override fun onSuccess() { + jwksJson.set(fetchedJwks) + onFetchSuccess() + } + + override fun onThrowable(error: Throwable) { + setInvalidResult( + UiToolsBundle.message( + "jwt-encoder-decoder.validation.fetch-jwks.failed", + rootCauseMessage(error), + ) + ) + } + + override fun onFinished() { + fetchingJwks.set(false) + } + } + .queue() + } + + fun validate(encodedJwt: String) { + if (encodedJwt.isBlank()) { + tokenMetadata.set( + UiToolsBundle.message("jwt-encoder-decoder.validation.token-metadata.default") + ) + setNeutralResult() + return + } + + try { + val jwtParts = encodedJwt.split('.', limit = 3) + if (jwtParts.size < 2) { + tokenMetadata.set( + UiToolsBundle.message("jwt-encoder-decoder.validation.token-metadata.unknown") + ) + setInvalidResult( + UiToolsBundle.message("jwt-encoder-decoder.validation.invalid-compact-jwt.header-payload") + ) + return + } + + val headerNode = + ObjectMapperService.instance.jsonMapper().readTree(jwtParts[0].decodeBase64String()) + val algorithm = + headerNode.get("alg")?.asText()?.let { SignatureAlgorithm.findByJwtHeaderValue(it) } + ?: run { + tokenMetadata.set( + UiToolsBundle.message("jwt-encoder-decoder.validation.token-metadata.unknown") + ) + setInvalidResult( + UiToolsBundle.message( + "jwt-encoder-decoder.validation.missing-or-unsupported-alg-header" + ) + ) + return + } + val keyId = headerNode.get("kid")?.asText()?.takeIf { it.isNotBlank() } + tokenMetadata.set( + buildString { + append( + UiToolsBundle.message( + "jwt-encoder-decoder.validation.token-metadata.algorithm", + algorithm.jwtHeaderValue, + ) + ) + if (keyId != null) { + append( + UiToolsBundle.message( + "jwt-encoder-decoder.validation.token-metadata.kid-suffix", + keyId, + ) + ) + } + } + ) + + if (algorithm == SignatureAlgorithm.NONE) { + if (jwtParts.getOrElse(2) { "" }.isEmpty()) { + setValidResult( + UiToolsBundle.message("jwt-encoder-decoder.validation.valid-unsecured-jwt") + ) + } else { + setInvalidResult( + UiToolsBundle.message("jwt-encoder-decoder.validation.invalid-jwt.alg-none-signature") + ) + } + return + } + + if (jwtParts.size < 3) { + setInvalidResult( + UiToolsBundle.message("jwt-encoder-decoder.validation.invalid-compact-jwt.signature") + ) + return + } + + val outcome = + when (keySource.get()) { + ValidationKeySource.SECRET -> validateWithSecret(encodedJwt, algorithm) + ValidationKeySource.PUBLIC_KEY -> validateWithPublicKey(encodedJwt, algorithm) + ValidationKeySource.JWKS -> validateWithJwks(encodedJwt, algorithm, keyId) + } + if (outcome.valid) { + setValidResult(outcome.message) + } else { + setInvalidResult(outcome.message) + } + } catch (e: Exception) { + tokenMetadata.set( + UiToolsBundle.message("jwt-encoder-decoder.validation.token-metadata.unknown") + ) + setInvalidResult( + UiToolsBundle.message("jwt-encoder-decoder.validation.failed", rootCauseMessage(e)) + ) + } + } + + private fun validateWithSecret( + encodedJwt: String, + algorithm: SignatureAlgorithm, + ): ValidationOutcome { + if (algorithm.kind != SignatureAlgorithmKind.HMAC) { + return ValidationOutcome( + UiToolsBundle.message( + "jwt-encoder-decoder.validation.key-source-mismatch.public-key-or-jwks", + algorithm.jwtHeaderValue, + ), + false, + ) + } + + val signingKey = + HmacKey( + when (secretEncodingMode.get()) { + SecretKeyEncodingMode.RAW -> secret.get().encodeToByteArray() + SecretKeyEncodingMode.BASE32 -> Base32().decode(secret.get()) + SecretKeyEncodingMode.BASE64 -> Base64.getDecoder().decode(secret.get()) + } + ) + return if (verifySignature(encodedJwt, signingKey)) { + ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.signature.valid"), + true, + ) + } else { + ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.signature.invalid"), + false, + ) + } + } + + private fun validateWithPublicKey( + encodedJwt: String, + algorithm: SignatureAlgorithm, + ): ValidationOutcome { + if (algorithm.kind == SignatureAlgorithmKind.HMAC) { + return ValidationOutcome( + UiToolsBundle.message( + "jwt-encoder-decoder.validation.key-source-mismatch.shared-secret", + algorithm.jwtHeaderValue, + ), + false, + ) + } + + val keyInput = publicKey.get().trim() + if (keyInput.isBlank()) { + return ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.provide-public-key-or-jwk"), + false, + ) + } + + val verificationKey = + try { + readVerificationKey(keyInput, algorithm) + } catch (e: Exception) { + return ValidationOutcome( + UiToolsBundle.message( + "jwt-encoder-decoder.validation.read-public-key-or-jwk.failed", + rootCauseMessage(e), + ), + false, + ) + } + + return if (verifySignature(encodedJwt, verificationKey)) { + ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.signature.valid"), + true, + ) + } else { + ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.signature.invalid"), + false, + ) + } + } + + private fun validateWithJwks( + encodedJwt: String, + algorithm: SignatureAlgorithm, + keyId: String?, + ): ValidationOutcome { + val jwksValue = jwksJson.get().trim() + if (jwksValue.isBlank()) { + return ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.provide-jwks-json"), + false, + ) + } + + val jwks = + try { + JsonWebKeySet(jwksValue) + } catch (e: Exception) { + return ValidationOutcome( + UiToolsBundle.message( + "jwt-encoder-decoder.validation.parse-jwks.failed", + rootCauseMessage(e), + ), + false, + ) + } + + val candidateKeys = + jwks.jsonWebKeys.filter { + (keyId == null || keyId == it.keyId) && isJwkCompatibleWithAlgorithm(it, algorithm) + } + + if (candidateKeys.isEmpty()) { + return if (keyId != null) { + ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.no-compatible-key-with-kid", keyId), + false, + ) + } else { + ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.no-compatible-key"), + false, + ) + } + } + + candidateKeys.forEach { jsonWebKey -> + try { + if (verifySignature(encodedJwt, jsonWebKey.key)) { + return if (jsonWebKey.keyId != null) { + ValidationOutcome( + UiToolsBundle.message( + "jwt-encoder-decoder.validation.signature.valid-using-key", + jsonWebKey.keyId, + ), + true, + ) + } else { + ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.signature.valid"), + true, + ) + } + } + } catch (_: Exception) { + // Ignore incompatible keys from the JWKS and continue with the next candidate. + } + } + + return ValidationOutcome( + UiToolsBundle.message("jwt-encoder-decoder.validation.signature.invalid"), + false, + ) + } + + private fun verifySignature(encodedJwt: String, key: Key): Boolean = + JsonWebSignature() + .apply { + setCompactSerialization(encodedJwt) + setKey(key) + isDoKeyValidation = strictKeyValidation.get() + } + .verifySignature() + + private fun readVerificationKey(keyInput: String, algorithm: SignatureAlgorithm): Key = + if (keyInput.startsWith("{")) { + JsonWebKey.Factory.newJwk(keyInput).key + } else { + algorithm.kind.keyFactory!!.generatePublic(X509EncodedKeySpec(toRawKey(keyInput))) + } + + private fun isJwkCompatibleWithAlgorithm( + jsonWebKey: JsonWebKey, + algorithm: SignatureAlgorithm, + ): Boolean { + val algorithmMatches = + jsonWebKey.algorithm == null || jsonWebKey.algorithm == algorithm.jwtHeaderValue + val useMatches = jsonWebKey.use == null || jsonWebKey.use == "sig" + val keyTypeMatches = + when (algorithm.kind) { + SignatureAlgorithmKind.NONE -> false + SignatureAlgorithmKind.HMAC -> jsonWebKey.keyType == "oct" + SignatureAlgorithmKind.RSA -> jsonWebKey.keyType == "RSA" + SignatureAlgorithmKind.ECDSA -> jsonWebKey.keyType == "EC" + } + return algorithmMatches && useMatches && keyTypeMatches + } + + private fun toRawKey(keyInput: String): ByteArray = + Base64.getDecoder().decode(keyInput.replace(JwtEncoderDecoder.rawKeyRegex, "")) + + private fun rootCauseMessage(exception: Throwable): String { + val rootCause = ExceptionUtil.getRootCause(exception) + return rootCause.message ?: rootCause::class.simpleName ?: "unknown" + } + + private fun setNeutralResult() { + resultState.set(ValidationResultState.NONE) + validResultMessage.set("") + invalidResultMessage.set("") + } + + private fun setValidResult(message: String) { + resultState.set(ValidationResultState.VALID) + validResultMessage.set(" $message") + invalidResultMessage.set("") + } + + private fun setInvalidResult(message: String) { + resultState.set(ValidationResultState.INVALID) + validResultMessage.set("") + invalidResultMessage.set(" $message") + } + + private data class ValidationOutcome(val message: String, val valid: Boolean) +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AboutPluginDialog.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AboutPluginDialog.kt index f3083cb9..8f10a3af 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AboutPluginDialog.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AboutPluginDialog.kt @@ -11,6 +11,7 @@ import com.intellij.ui.dsl.builder.TopGap import com.intellij.ui.dsl.builder.panel import com.intellij.util.ui.JBEmptyBorder import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginInfo +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.applyDefaultTabComponentInsets import java.awt.Dimension import javax.swing.Action import javax.swing.JComponent @@ -36,6 +37,8 @@ class AboutPluginDialog(project: Project?, parentComponent: JComponent) : cell( JBTabbedPane().apply { + applyDefaultTabComponentInsets() + tabs.forEach { (title, component) -> // Create scroll panes with specific preferred size val scrollPane = ScrollPaneFactory.createScrollPane(component, true) diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/BarcodeGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/BarcodeGenerator.kt index 4fa1dcce..50445413 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/BarcodeGenerator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/BarcodeGenerator.kt @@ -130,7 +130,7 @@ private constructor( lateinit var backgroundColorButton: JButton backgroundColorButton = button("Change") { - ColorChooserService.instance + ColorChooserService.getInstance() .showDialog( project, backgroundColorButton, @@ -149,7 +149,7 @@ private constructor( lateinit var foregroundColorButton: JButton foregroundColorButton = button("Change") { - ColorChooserService.instance + ColorChooserService.getInstance() .showDialog( project, foregroundColorButton, diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ExternalSystemProcessRegistry.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ExternalSystemProcessRegistry.kt new file mode 100644 index 00000000..d48ff5ba --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ExternalSystemProcessRegistry.kt @@ -0,0 +1,114 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.intellij.execution.process.KillableProcessHandler +import com.intellij.execution.process.ProcessHandler +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.project.Project + +@Service(Service.Level.APP) +class ExternalSystemProcessRegistry : Disposable { + // -- Properties ---------------------------------------------------------- // + + private val log = logger() + private val lock = Any() + private val projectsByProcess = LinkedHashMap() + private val processesByProject = LinkedHashMap>() + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + fun register(project: Project?, process: ProcessHandler) { + synchronized(lock) { + projectsByProcess.put(process, project)?.let { previousProject -> + removeTrackedProcess(process, previousProject) + } + processesByProject.getOrPut(project) { linkedSetOf() }.add(process) + } + } + + fun unregister(process: ProcessHandler) { + synchronized(lock) { + projectsByProcess.remove(process)?.let { project -> removeTrackedProcess(process, project) } + } + } + + fun stopProcesses(project: Project) { + trackedProcesses(project).forEach { stopTrackedProcess(it) } + } + + override fun dispose() { + trackedProcesses().forEach { stopTrackedProcess(it) } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun trackedProcesses(project: Project): List = + synchronized(lock) { + processesByProject.remove(project)?.toList().orEmpty().onEach { projectsByProcess.remove(it) } + } + + private fun trackedProcesses(): List = + synchronized(lock) { + projectsByProcess.keys.toList().also { + projectsByProcess.clear() + processesByProject.clear() + } + } + + private fun removeTrackedProcess(process: ProcessHandler, project: Project?) { + processesByProject[project]?.apply { + remove(process) + if (isEmpty()) { + processesByProject.remove(project) + } + } + } + + private fun stopTrackedProcess(process: ProcessHandler) { + if (process.isProcessTerminated) { + return + } + + runCatching { + process.destroyProcess() + if (!process.waitFor(STOP_TIMEOUT_MILLISECONDS)) { + if (process is KillableProcessHandler && process.canKillProcess()) { + process.killProcess() + } + if (!process.waitFor(STOP_TIMEOUT_MILLISECONDS)) { + throw IllegalStateException("Timed out while stopping tracked HttpServer process") + } + } + } + .onFailure { error -> log.warn("Failed to stop tracked HttpServer process", error) } + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val STOP_TIMEOUT_MILLISECONDS = 5_000L + } +} + +@Service(Service.Level.PROJECT) +class HttpServerProjectProcessShutdownService(private val project: Project) : Disposable { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun dispose() { + ApplicationManager.getApplication() + .service() + .stopProcesses(project) + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/HttpServer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/HttpServer.kt new file mode 100644 index 00000000..2a96769c --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/HttpServer.kt @@ -0,0 +1,1075 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.KillableProcessHandler +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.process.ProcessListener +import com.intellij.icons.AllIcons +import com.intellij.ide.BrowserUtil +import com.intellij.json.JsonLanguage +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.Key +import com.intellij.ui.HyperlinkLabel +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTabbedPane +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.selected +import com.intellij.util.ui.components.BorderLayoutPanel +import dev.turingcomplete.intellijdevelopertoolsplugin.common.OkHttpClientUtils.applyIntelliJProxySettings +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.ObjectMapperService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.PropertyComponentPredicate +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.applyDefaultTabComponentInsets +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bind +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindIntTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateLongValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import java.awt.Dimension +import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import javax.swing.JButton +import javax.swing.JComponent +import javax.swing.event.HyperlinkEvent +import okhttp3.OkHttpClient +import okhttp3.Request + +class HttpServer( + private val configuration: DeveloperToolConfiguration, + private val project: Project?, + private val context: DeveloperUiToolContext, + parentDisposable: Disposable, +) : DeveloperUiTool(parentDisposable) { + // -- Properties ---------------------------------------------------------- // + + private val log = logger() + private val content = BorderLayoutPanel() + private val serverStatus = + ValueProperty(UiToolsBundle.message("http-server.server.status.stopped")) + private val serverUrl = ValueProperty("") + private val serverRunning = ValueProperty(false) + private val serverBusy = ValueProperty(false) + private val restartRequired = ValueProperty(false) + + private val serverPort = + configuration.register("serverPort", DEFAULT_WIREMOCK_PORT, CONFIGURATION) + private val verboseLogging = configuration.register("verboseLogging", false, CONFIGURATION) + private val printAllNetworkTraffic = + configuration.register("printAllNetworkTraffic", false, CONFIGURATION) + private val advancedCommandLineOptions = + configuration.register( + "advancedCommandLineOptions", + DEFAULT_ADVANCED_COMMAND_LINE_OPTIONS, + CONFIGURATION, + ) + private val javaExecutableMode = + configuration.register("javaExecutableMode", JavaExecutableMode.BUILT_IN_JRE, CONFIGURATION) + private val javaExecutablePath = configuration.register("javaExecutablePath", "", CONFIGURATION) + private val serverMode = configuration.register("serverMode", ServerMode.BUILT_IN_SERVER) + private val customRootDirectory = configuration.register("customRootDirectory", "", CONFIGURATION) + private val builtInServerMapping = + configuration.register( + "builtInServerMapping", + "", + CONFIGURATION, + """ + { + "request": { + "method": "GET", + "urlPattern": "/" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "text/plain" + }, + "body": "Hello World!" + } + } + """ + .trimIndent(), + ) + + private val builtInServerMappingEditor = + AdvancedEditor( + id = "builtInServerMapping", + context = context, + configuration = configuration, + project = project, + title = null, + editorMode = AdvancedEditor.EditorMode.INPUT, + parentDisposable = parentDisposable, + textProperty = builtInServerMapping, + initialLanguage = JsonLanguage.INSTANCE, + minimumSizeHeight = 220, + ) + private val outputEditor = + AdvancedEditor( + id = "httpServerOutput", + context = context, + configuration = configuration, + project = project, + title = null, + editorMode = AdvancedEditor.EditorMode.OUTPUT, + parentDisposable = parentDisposable, + ) + + private val processLock = Any() + + @Volatile private var wireMockProcess: KillableProcessHandler? = null + private var startedServerMode: ServerMode? = null + private var startedServerPort: Int? = null + private var startedVerboseLogging: Boolean? = null + private var startedPrintAllNetworkTraffic: Boolean? = null + private var startedAdvancedCommandLineOptions: String? = null + private var startedJavaExecutableMode: JavaExecutableMode? = null + private var startedJavaExecutablePath: String? = null + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + init { + serverPort.afterChange(parentDisposable) { updateRestartWarning() } + verboseLogging.afterChange(parentDisposable) { updateRestartWarning() } + printAllNetworkTraffic.afterChange(parentDisposable) { updateRestartWarning() } + advancedCommandLineOptions.afterChange(parentDisposable) { updateRestartWarning() } + javaExecutableMode.afterChange(parentDisposable) { updateRestartWarning() } + javaExecutablePath.afterChange(parentDisposable) { updateRestartWarning() } + serverMode.afterChange(parentDisposable) { updateRestartWarning() } + serverRunning.afterChange(parentDisposable) { updateRestartWarning() } + + builtInServerMapping.afterChange(parentDisposable) { syncBuiltInServerMappingsIfNeeded() } + } + + override fun Panel.buildUi() { + row { cell(content).resizableColumn().align(Align.FILL) }.resizableRow() + } + + override fun afterBuildUi() { + syncContent() + } + + // -- Private Methods ----------------------------------------------------- // + + private fun syncContent() { + setContent(if (isWireMockDownloaded()) createConfigurationUi() else createDownloadUi()) + } + + private fun setContent(component: JComponent) { + runOnEdt { + content.removeAll() + content.addToCenter(component) + content.revalidate() + content.repaint() + } + } + + private fun createDownloadUi(): JComponent = panel { + row { cell(createDownloadDescription()).resizableColumn().align(Align.FILL) } + + row { cell(createMavenCentralLink()) }.topGap(TopGap.NONE) + + row { + lateinit var downloadButton: JButton + downloadButton = + button(UiToolsBundle.message("http-server.download.button")) { + downloadWireMockStandalone(downloadButton) + } + .component + } + } + + private fun createDownloadDescription(): JComponent = + JBLabel("${UiToolsBundle.message("http-server.download.description")}") + + private fun createMavenCentralLink(): JComponent = + HyperlinkLabel(UiToolsBundle.message("http-server.download.maven-central-link")).apply { + setHyperlinkTarget(MAVEN_CENTRAL_HTTP_REPOSITORY_URL) + } + + private fun createConfigurationUi(): JComponent = panel { + row { + label("") + .label(UiToolsBundle.message("http-server.server.status.label")) + .bindText(serverStatus) + + button(UiToolsBundle.message("http-server.server.start")) { startWireMock() } + .visibleIf(PropertyComponentPredicate(serverRunning, false)) + .enabledIf(PropertyComponentPredicate(serverBusy, false)) + .gap(RightGap.SMALL) + + button(UiToolsBundle.message("http-server.server.restart")) { restartWireMock() } + .visibleIf(PropertyComponentPredicate(serverRunning, true)) + .enabledIf(PropertyComponentPredicate(serverBusy, false)) + .gap(RightGap.SMALL) + + button(UiToolsBundle.message("http-server.server.stop")) { stopWireMock() } + .visibleIf(PropertyComponentPredicate(serverRunning, true)) + .enabledIf(PropertyComponentPredicate(serverBusy, false)) + .gap(RightGap.SMALL) + + contextHelp(UiToolsBundle.message("http-server.server.auto-reload-info")) + } + + row { cell(createServerUrlLink()) }.visibleIf(PropertyComponentPredicate(serverRunning, true)) + + row { + icon(AllIcons.General.Warning).gap(RightGap.SMALL) + label(UiToolsBundle.message("http-server.server.restart-required")) + } + .visibleIf(PropertyComponentPredicate(restartRequired, true)) + + row { + cell( + JBTabbedPane().apply { + applyDefaultTabComponentInsets() + + addTab( + UiToolsBundle.message("http-server.tab.configuration"), + createConfigurationTab(), + ) + addTab(UiToolsBundle.message("http-server.tab.output"), createOutputTab()) + } + ) + .resizableColumn() + .align(Align.FILL) + } + .resizableRow() + } + + private fun createConfigurationTab(): JComponent = panel { + row { + textField() + .label(UiToolsBundle.message("http-server.server.port.label")) + .bindIntTextImproved(serverPort) + .validateLongValue(LongRange(1, 65_535)) + .columns(6) + } + + row { + checkBox(UiToolsBundle.message("http-server.server.verbose-logging")) + .bindSelected(verboseLogging) + } + + row { + checkBox(UiToolsBundle.message("http-server.server.print-all-network-traffic")) + .bindSelected(printAllNetworkTraffic) + } + + row { cell(createAdvancedConfigLink()) }.topGap(TopGap.NONE) + + buttonsGroup { + row { + val customDirectoryRadioButton = + radioButton(UiToolsBundle.message("http-server.configuration.mode.custom-directory")) + .bind(serverMode, ServerMode.CUSTOM_DIRECTORY) + .gap(RightGap.SMALL) + textFieldWithBrowseButton( + FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle(UiToolsBundle.message("http-server.configuration.custom-directory.title")), + project, + ) + .bindText(customRootDirectory) + .enabledIf(customDirectoryRadioButton.selected) + .resizableColumn() + .align(Align.FILL) + .gap(RightGap.SMALL) + } + + row { + radioButton(UiToolsBundle.message("http-server.configuration.mode.built-in-server")) + .bind(serverMode, ServerMode.BUILT_IN_SERVER) + } + } + + row { cell(builtInServerMappingEditor.component).resizableColumn().align(Align.FILL) } + .resizableRow() + .topGap(TopGap.SMALL) + .visibleIf(PropertyComponentPredicate(serverMode, ServerMode.BUILT_IN_SERVER)) + + row { cell(createBuiltInServerDirectoryLink()) } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + .visibleIf(PropertyComponentPredicate(serverMode, ServerMode.BUILT_IN_SERVER)) + + row { cell(createMappingsDocumentationLink()) }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE) + } + + private fun createOutputTab(): JComponent = panel { + row { cell(outputEditor.component).resizableColumn().align(Align.FILL) } + .layout(RowLayout.INDEPENDENT) + .resizableRow() + + row { cell(createClearOutputLink()) } + } + + private fun createClearOutputLink(): JComponent = + HyperlinkLabel(UiToolsBundle.message("http-server.output.clear")).apply { + addHyperlinkListener { event -> + if (event.eventType == HyperlinkEvent.EventType.ACTIVATED) { + clearProcessOutput() + } + } + } + + private fun createMappingsDocumentationLink(): JComponent = + HyperlinkLabel().apply { + setTextWithHyperlink( + UiToolsBundle.message("http-server.configuration.mappings.documentation") + ) + setHyperlinkTarget(WIREMOCK_MAPPINGS_DOCUMENTATION_URL) + } + + private fun createCommandLineOptionsDocumentationLink(): JComponent = + HyperlinkLabel().apply { + setTextWithHyperlink(UiToolsBundle.message("http-server.advanced-config.documentation")) + setHyperlinkTarget(WIREMOCK_COMMAND_LINE_OPTIONS_DOCUMENTATION_URL) + } + + private fun createAdvancedConfigLink(): JComponent = + HyperlinkLabel(UiToolsBundle.message("http-server.server.advanced-config")).apply { + addHyperlinkListener { event -> + if (event.eventType == HyperlinkEvent.EventType.ACTIVATED) { + showAdvancedConfigDialog() + } + } + } + + private fun createBuiltInServerDirectoryLink(): JComponent = + HyperlinkLabel(UiToolsBundle.message("http-server.configuration.built-in-server.directory")) + .apply { + addHyperlinkListener { event -> + if (event.eventType == HyperlinkEvent.EventType.ACTIVATED) { + Files.createDirectories(builtInServerFilesDirectory) + BrowserUtil.browse(builtInServerRootDirectory) + } + } + } + + private fun createServerUrlLink(): JComponent = + HyperlinkLabel().apply { + updateServerUrlLink(serverUrl.get()) + addHyperlinkListener { event -> + if (event.eventType == HyperlinkEvent.EventType.ACTIVATED) { + BrowserUtil.open(serverUrl.get()) + } + } + serverUrl.afterChange(parentDisposable) { updateServerUrlLink(it) } + } + + private fun HyperlinkLabel.updateServerUrlLink(url: String) { + setTextWithHyperlink("$url") + setHyperlinkTarget(url) + } + + private fun downloadWireMockStandalone(downloadButton: JButton) { + object : + Task.Backgroundable(project, UiToolsBundle.message("http-server.download.task-title")) { + + override fun run(indicator: ProgressIndicator) { + setButtonEnabled(downloadButton, false) + indicator.isIndeterminate = true + indicator.text = UiToolsBundle.message("http-server.download.task-title") + + downloadWireMockStandaloneJar() + } + + override fun onSuccess() { + syncContent() + } + + override fun onThrowable(error: Throwable) { + log.warn("Failed to download WireMock standalone to: $wireMockStandaloneJarPath", error) + + Messages.showErrorDialog( + project, + UiToolsBundle.message( + "http-server.download.failed", + "${error::class.simpleName ?: error::class.java.simpleName}: ${error.message ?: "Unknown error"}", + ), + UiToolsBundle.message("http-server.download.failed-title"), + ) + } + + override fun onFinished() { + if (!isWireMockDownloaded()) { + setButtonEnabled(downloadButton, true) + } + } + } + .queue() + } + + private fun downloadWireMockStandaloneJar() { + val httpClient = + OkHttpClient.Builder().applyIntelliJProxySettings(WIREMOCK_STANDALONE_DOWNLOAD_URL).build() + val request = Request.Builder().get().url(WIREMOCK_STANDALONE_DOWNLOAD_URL).build() + + Files.createDirectories(wireMockStandaloneJarPath.parent) + val temporaryDownloadPath = + wireMockStandaloneJarPath.resolveSibling("${wireMockStandaloneJarPath.fileName}.part") + + try { + Files.deleteIfExists(temporaryDownloadPath) + + httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IllegalStateException( + "Failed to download WireMock standalone from $WIREMOCK_STANDALONE_DOWNLOAD_URL: HTTP ${response.code}" + ) + } + + response.body.byteStream().use { inputStream -> + Files.copy(inputStream, temporaryDownloadPath, StandardCopyOption.REPLACE_EXISTING) + } + } + + Files.move( + temporaryDownloadPath, + wireMockStandaloneJarPath, + StandardCopyOption.REPLACE_EXISTING, + ) + } catch (e: Throwable) { + try { + Files.deleteIfExists(temporaryDownloadPath) + } catch (cleanupError: Exception) { + log.warn( + "Failed to delete temporary WireMock download: $temporaryDownloadPath", + cleanupError, + ) + } + + throw e + } + } + + private fun isWireMockDownloaded(): Boolean = + Files.isRegularFile(wireMockStandaloneJarPath) && + runCatching { Files.size(wireMockStandaloneJarPath) > 0L }.getOrDefault(false) + + private fun syncBuiltInServerMappingsIfNeeded() { + if (serverMode.get() != ServerMode.BUILT_IN_SERVER || !isWireMockDownloaded()) { + return + } + + ApplicationManager.getApplication().executeOnPooledThread { + runCatching { prepareBuiltInServerRootDirectory() } + .onFailure { error -> log.warn("Failed to update generated WireMock mappings", error) } + } + } + + private fun startWireMock() { + if (serverBusy.get()) { + return + } + + appendProcessEvent("Starting WireMock") + startOrRestartWireMock( + busyStatus = UiToolsBundle.message("http-server.server.status.starting"), + failureLogMessage = "Failed to start WireMock", + failureTitle = UiToolsBundle.message("http-server.server.start.failed-title"), + createFailureMessage = { errorMessage -> + UiToolsBundle.message("http-server.server.start.failed", errorMessage) + }, + ) + } + + private fun restartWireMock() { + if (serverBusy.get()) { + return + } + + val process = currentWireMockProcess() + if (process == null) { + startWireMock() + return + } + + appendProcessEvent("Restarting WireMock") + startOrRestartWireMock( + processToStop = process, + busyStatus = UiToolsBundle.message("http-server.server.status.restarting"), + failureLogMessage = "Failed to restart WireMock", + failureTitle = UiToolsBundle.message("http-server.server.restart.failed-title"), + createFailureMessage = { errorMessage -> + UiToolsBundle.message("http-server.server.restart.failed", errorMessage) + }, + ) + } + + private fun stopWireMock() { + if (serverBusy.get()) { + return + } + + val process = currentWireMockProcess() ?: return + + setServerBusy(true, UiToolsBundle.message("http-server.server.status.stopping")) + appendProcessEvent("Stopping WireMock") + + ApplicationManager.getApplication().executeOnPooledThread { + try { + stopWireMockProcess(process) + appendProcessEvent("WireMock stopped") + setServerRunning(false, UiToolsBundle.message("http-server.server.status.stopped")) + } catch (e: Exception) { + log.warn("Failed to stop WireMock", e) + appendProcessEvent("Failed to stop WireMock: ${e.message ?: "Unknown error"}") + showWireMockErrorDialog( + UiToolsBundle.message("http-server.server.stop.failed-title"), + UiToolsBundle.message("http-server.server.stop.failed", e.message ?: "Unknown error"), + ) + } finally { + setServerBusy(false) + } + } + } + + private fun startOrRestartWireMock( + processToStop: KillableProcessHandler? = null, + busyStatus: String, + failureLogMessage: String, + failureTitle: String, + createFailureMessage: (String) -> String, + ) { + setServerBusy(true, busyStatus) + + ApplicationManager.getApplication().executeOnPooledThread { + try { + processToStop?.let { stopWireMockProcess(it) } + + val rootDirectory = prepareRootDirectoryForCurrentConfiguration() + startWireMockProcess(rootDirectory) + startedServerMode = serverMode.get() + startedServerPort = serverPort.get() + startedVerboseLogging = verboseLogging.get() + startedPrintAllNetworkTraffic = printAllNetworkTraffic.get() + startedAdvancedCommandLineOptions = advancedCommandLineOptions.get() + startedJavaExecutableMode = javaExecutableMode.get() + startedJavaExecutablePath = javaExecutablePath.get() + + appendProcessEvent("WireMock running at ${currentServerUrl()}") + setServerRunning( + true, + UiToolsBundle.message("http-server.server.status.running"), + currentServerUrl(), + ) + } catch (e: Exception) { + log.warn(failureLogMessage, e) + appendProcessEvent(failureSummaryMessage(e)) + setServerRunning(false, UiToolsBundle.message("http-server.server.status.stopped")) + showWireMockErrorDialog(failureTitle, createFailureMessage(failureDetailedMessage(e))) + } finally { + setServerBusy(false) + } + } + } + + private fun prepareRootDirectoryForCurrentConfiguration(): Path = + when (serverMode.get()) { + ServerMode.CUSTOM_DIRECTORY -> resolveCustomRootDirectory() + ServerMode.BUILT_IN_SERVER -> prepareBuiltInServerRootDirectory() + } + + private fun resolveCustomRootDirectory(): Path { + val customRootDirectory = customRootDirectory.get().trim() + check(customRootDirectory.isNotEmpty()) { + UiToolsBundle.message("http-server.configuration.custom-directory.missing") + } + + val rootDirectory = + try { + Paths.get(customRootDirectory) + } catch (_: InvalidPathException) { + throw IllegalStateException( + UiToolsBundle.message("http-server.configuration.custom-directory.invalid") + ) + } + + if (Files.exists(rootDirectory) && !Files.isDirectory(rootDirectory)) { + throw IllegalStateException( + UiToolsBundle.message("http-server.configuration.custom-directory.invalid") + ) + } + + Files.createDirectories(rootDirectory) + + return rootDirectory + } + + private fun prepareBuiltInServerRootDirectory(): Path { + Files.createDirectories(builtInServerMappingsDirectory) + deleteGeneratedBuiltInServerMappings() + writeJson( + builtInServerMappingsDirectory.resolve("built-in-server-mapping.json"), + builtInServerMapping.get(), + ) + + return builtInServerRootDirectory + } + + private fun deleteGeneratedBuiltInServerMappings() { + Files.list(builtInServerMappingsDirectory).use { mappingFiles -> + mappingFiles.filter { Files.isRegularFile(it) }.forEach { Files.deleteIfExists(it) } + } + } + + private fun writeJson(file: Path, content: Any) { + Files.createDirectories(file.parent) + ObjectMapperService.instance + .jsonMapper() + .writerWithDefaultPrettyPrinter() + .writeValue( + file.toFile(), + if (content is String) { + ObjectMapperService.instance.jsonMapper().readTree(content) + } else { + content + }, + ) + } + + private fun startWireMockProcess(rootDirectory: Path): KillableProcessHandler { + check(isWireMockDownloaded()) { + UiToolsBundle.message("http-server.download.missing-start-blocked") + } + + val javaExecutable = currentJavaExecutable() + val parameters = buildList { + add("-jar") + add(wireMockStandaloneJarPath.toString()) + add("--port") + add(serverPort.get().toString()) + add("--root-dir") + add(rootDirectory.toAbsolutePath().normalize().toString()) + if (serverMode.get() == ServerMode.BUILT_IN_SERVER) { + add("--local-response-templating") + } + if (verboseLogging.get()) { + add("--verbose") + } + if (printAllNetworkTraffic.get()) { + add("--print-all-network-traffic") + } + addAll(parseAdvancedCommandLineOptions()) + } + + val commandLine = + GeneralCommandLine() + .withExePath(javaExecutable.toString()) + .withParameters(parameters) + .withRedirectErrorStream(true) + + appendProcessEvent("Command: ${commandLine.commandLineString}") + + val processOutput = StringBuilder() + val processHandler = KillableProcessHandler(commandLine) + consumeProcessOutput(processHandler, processOutput) + watchWireMockProcess(processHandler) + synchronized(processLock) { wireMockProcess = processHandler } + registerWireMockProcess(processHandler) + processHandler.startNotify() + + if (processHandler.waitFor(STARTUP_TIMEOUT_MILLISECONDS)) { + val exitCode = processHandler.exitCode ?: -1 + unregisterWireMockProcess(processHandler) + synchronized(processLock) { + if (wireMockProcess === processHandler) { + wireMockProcess = null + } + } + throw IllegalStateException( + UiToolsBundle.message( + "http-server.server.start.process-exited", + exitCode, + buildProcessOutputMessage(processOutput), + ) + ) + } + + return processHandler + } + + private fun consumeProcessOutput( + processHandler: KillableProcessHandler, + processOutput: StringBuilder, + ) { + processHandler.addProcessListener( + object : ProcessListener { + + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { + val outputChunk = event.text + synchronized(processOutput) { + processOutput.append(outputChunk) + if (processOutput.length > MAX_STARTUP_PROCESS_OUTPUT_LENGTH) { + processOutput.delete(0, processOutput.length - MAX_STARTUP_PROCESS_OUTPUT_LENGTH) + } + } + outputEditor.appendText(outputChunk) + } + } + ) + } + + private fun watchWireMockProcess(processHandler: KillableProcessHandler) { + processHandler.addProcessListener( + object : ProcessListener { + + override fun processTerminated(event: ProcessEvent) { + unregisterWireMockProcess(processHandler) + val wasCurrentProcess = + synchronized(processLock) { + if (wireMockProcess === processHandler) { + wireMockProcess = null + true + } else { + false + } + } + + if (wasCurrentProcess && !serverBusy.get() && !isDisposed) { + appendProcessEvent("WireMock exited with code ${event.exitCode}") + setServerRunning( + false, + UiToolsBundle.message("http-server.server.status.stopped.exit-code", event.exitCode), + ) + } + } + } + ) + } + + private fun stopWireMockProcess(process: KillableProcessHandler) { + synchronized(processLock) { + if (wireMockProcess === process) { + wireMockProcess = null + } + } + unregisterWireMockProcess(process) + + process.destroyProcess() + if (!process.waitFor(STOP_TIMEOUT_MILLISECONDS)) { + if (process.canKillProcess()) { + process.killProcess() + } + if (!process.waitFor(STOP_TIMEOUT_MILLISECONDS)) { + throw IllegalStateException(UiToolsBundle.message("http-server.server.stop.timeout")) + } + } + } + + private fun currentWireMockProcess(): KillableProcessHandler? = + synchronized(processLock) { + val process = wireMockProcess + if (process != null && process.isProcessTerminated) { + wireMockProcess = null + null + } else { + process + } + } + + private fun registerWireMockProcess(process: KillableProcessHandler) { + project?.service() + ApplicationManager.getApplication() + .service() + .register(project, process) + } + + private fun unregisterWireMockProcess(process: KillableProcessHandler) { + ApplicationManager.getApplication().service().unregister(process) + } + + private fun currentJavaExecutable(): Path { + if (javaExecutableMode.get() == JavaExecutableMode.PATH) { + val customJavaExecutable = javaExecutablePath.get().trim() + check(customJavaExecutable.isNotEmpty()) { + UiToolsBundle.message("http-server.advanced-config.java-executable.path.missing") + } + + return try { + Paths.get(customJavaExecutable) + } catch (_: InvalidPathException) { + throw IllegalStateException( + UiToolsBundle.message("http-server.advanced-config.java-executable.path.invalid") + ) + } + } + + val javaExecutableFileName = + if (System.getProperty("os.name").lowercase().contains("win")) "java.exe" else "java" + + return Paths.get(System.getProperty("java.home"), "bin", javaExecutableFileName) + } + + private fun buildProcessOutputMessage(processOutput: StringBuilder): String = + synchronized(processOutput) { + processOutput.toString().trim().ifBlank { + UiToolsBundle.message("http-server.server.process-output.empty") + } + } + + private fun failureSummaryMessage(error: Throwable): String { + val detailedMessage = failureDetailedMessage(error) + val summaryMessage = + detailedMessage.substringBefore("\n").let { firstLine -> + if (". " in firstLine) { + firstLine.substringBefore(". ") + "." + } else { + firstLine + } + } + + return summaryMessage.ifBlank { "Unknown error" } + } + + private fun failureDetailedMessage(error: Throwable): String = error.message ?: "Unknown error" + + private fun clearProcessOutput() { + outputEditor.clearText() + } + + private fun appendProcessEvent(message: String) { + val timestamp = LocalTime.now().format(processEventTimestampFormat) + outputEditor.appendText("[$timestamp] $message\n") + } + + private fun setServerBusy(busy: Boolean, status: String? = null) { + runOnEdt { + serverBusy.set(busy) + status?.let { serverStatus.set(it) } + } + } + + private fun setServerRunning(running: Boolean, status: String, url: String = "") { + runOnEdt { + serverRunning.set(running) + serverStatus.set(status) + serverUrl.set(url) + } + } + + private fun updateRestartWarning() { + val restartRequired = + (serverRunning.get() && + (startedServerPort != serverPort.get() || + startedServerMode != serverMode.get() || + startedVerboseLogging != verboseLogging.get() || + startedPrintAllNetworkTraffic != printAllNetworkTraffic.get() || + startedAdvancedCommandLineOptions != advancedCommandLineOptions.get() || + startedJavaExecutableMode != javaExecutableMode.get() || + (javaExecutableMode.get() == JavaExecutableMode.PATH && + startedJavaExecutablePath != javaExecutablePath.get()))) + + runOnEdt { this.restartRequired.set(restartRequired) } + } + + private fun showAdvancedConfigDialog() { + val commandLineOptions = ValueProperty(advancedCommandLineOptions.get()) + val selectedJavaExecutableMode = ValueProperty(javaExecutableMode.get()) + val selectedJavaExecutablePath = ValueProperty(javaExecutablePath.get()) + val commandLineOptionsEditor = + AdvancedEditor( + id = "httpServerAdvancedCommandLineOptions", + context = context, + configuration = configuration, + project = project, + title = null, + editorMode = AdvancedEditor.EditorMode.INPUT, + parentDisposable = parentDisposable, + textProperty = commandLineOptions, + minimumSizeHeight = 220, + ) + .apply { component.preferredSize = Dimension(700, 260) } + + val apply = + object : DialogWrapper(project, content, true, IdeModalityType.IDE) { + + init { + title = UiToolsBundle.message("http-server.advanced-config.title") + init() + } + + override fun createCenterPanel(): JComponent = panel { + row { label(UiToolsBundle.message("http-server.advanced-config.command-line-options")) } + + row { cell(commandLineOptionsEditor.component).align(Align.FILL).resizableColumn() } + .resizableRow() + + row { cell(createCommandLineOptionsDocumentationLink()) } + .topGap(TopGap.SMALL) + .bottomGap(BottomGap.NONE) + + buttonsGroup(UiToolsBundle.message("http-server.advanced-config.java-executable")) { + row { + radioButton( + UiToolsBundle.message( + "http-server.advanced-config.java-executable.built-in-jre" + ) + ) + .bind(selectedJavaExecutableMode, JavaExecutableMode.BUILT_IN_JRE) + } + + row { + radioButton( + UiToolsBundle.message("http-server.advanced-config.java-executable.path") + ) + .bind(selectedJavaExecutableMode, JavaExecutableMode.PATH) + .gap(RightGap.SMALL) + textFieldWithBrowseButton( + FileChooserDescriptorFactory.singleFile() + .withTitle( + UiToolsBundle.message( + "http-server.advanced-config.java-executable.path.title" + ) + ), + project, + ) + .bindText(selectedJavaExecutablePath) + .enabledIf( + PropertyComponentPredicate(selectedJavaExecutableMode, JavaExecutableMode.PATH) + ) + .resizableColumn() + .align(Align.FILL) + } + } + } + + override fun getDimensionServiceKey(): String = + "${HttpServer::class.java.name}.AdvancedConfigDialog" + } + .showAndGet() + + if (apply) { + advancedCommandLineOptions.set(commandLineOptions.get()) + javaExecutableMode.set(selectedJavaExecutableMode.get()) + javaExecutablePath.set(selectedJavaExecutablePath.get()) + } + } + + private fun parseAdvancedCommandLineOptions(): List = + advancedCommandLineOptions + .get() + .lineSequence() + .map { it.trim() } + .filter { it.isNotEmpty() } + .toList() + + private fun currentServerUrl(): String = "http://localhost:${serverPort.get()}" + + private fun showWireMockErrorDialog(title: String, message: String) { + runOnEdt { Messages.showErrorDialog(project, message, title) } + } + + private fun setButtonEnabled(downloadButton: JButton, enabled: Boolean) { + runOnEdt { downloadButton.isEnabled = enabled } + } + + private fun runOnEdt(action: () -> Unit) { + if (ApplicationManager.getApplication().isDispatchThread) { + action() + } else { + ApplicationManager.getApplication().invokeLater(action) + } + } + + override fun doDispose() { + currentWireMockProcess()?.let { process -> + runCatching { stopWireMockProcess(process) } + .onFailure { error -> log.warn("Failed to dispose running WireMock process", error) } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + enum class ServerMode { + + CUSTOM_DIRECTORY, + BUILT_IN_SERVER, + } + + enum class JavaExecutableMode { + + BUILT_IN_JRE, + PATH, + } + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("http-server.menu-title"), + contentTitle = UiToolsBundle.message("http-server.content-title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> HttpServer) = { configuration -> + HttpServer( + configuration = configuration, + project = project, + context = context, + parentDisposable = parentDisposable, + ) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val WIREMOCK_STANDALONE_VERSION = "3.13.2" + private const val MAVEN_CENTRAL_HTTP_REPOSITORY_URL = + "https://mvnrepository.com/artifact/org.wiremock/wiremock-standalone/$WIREMOCK_STANDALONE_VERSION" + private const val WIREMOCK_MAPPINGS_DOCUMENTATION_URL = "https://wiremock.org/docs/stubbing/" + private const val WIREMOCK_COMMAND_LINE_OPTIONS_DOCUMENTATION_URL = + "https://wiremock.org/docs/standalone/java-jar/#command-line-options" + private const val WIREMOCK_STANDALONE_DOWNLOAD_URL = + "https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/$WIREMOCK_STANDALONE_VERSION/wiremock-standalone-$WIREMOCK_STANDALONE_VERSION.jar" + private const val DEFAULT_WIREMOCK_PORT = 8089 + private const val STARTUP_TIMEOUT_MILLISECONDS = 1_500L + private const val STOP_TIMEOUT_MILLISECONDS = 5_000L + private const val MAX_STARTUP_PROCESS_OUTPUT_LENGTH = 8_000 + private const val DEFAULT_ADVANCED_COMMAND_LINE_OPTIONS = "--disable-banner" + private val processEventTimestampFormat = DateTimeFormatter.ofPattern("HH:mm:ss") + internal val httpServerToolRootPath: Path = + PathManager.getSystemDir().resolve(Paths.get("plugins", "developer-tools", "http-server")) + internal val wireMockStandaloneJarPath: Path = + httpServerToolRootPath.resolve("wiremock-standalone-$WIREMOCK_STANDALONE_VERSION.jar") + private val builtInServerRootDirectory = httpServerToolRootPath.resolve("built-in-server") + private val builtInServerMappingsDirectory = builtInServerRootDirectory.resolve("mappings") + private val builtInServerFilesDirectory = builtInServerRootDirectory.resolve("__files") + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/JsonSchemaValidator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/JsonSchemaValidator.kt index be2ce2f7..be74da00 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/JsonSchemaValidator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/JsonSchemaValidator.kt @@ -237,37 +237,37 @@ class JsonSchemaValidator( private val EXAMPLE_SCHEMA = """ -{ - "${'$'}id": "https://example.com/person.schema.json", - "${'$'}schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Person", - "type": "object", - "properties": { - "firstName": { - "type": "string", - "description": "The person's first name." - }, - "lastName": { - "type": "string", - "description": "The person's last name." - }, - "age": { - "description": "Age in years which must be equal to or greater than zero.", - "type": "integer", - "minimum": 0 - } - } -} - """ + { + "${'$'}id": "https://example.com/person.schema.json", + "${'$'}schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + } + } + } + """ .trimIndent() private val EXAMPLE_DATA = """ -{ - "firstName": "John", - "lastName": "Doe", - "age": 21 -} - """ + { + "firstName": "John", + "lastName": "Doe", + "age": 21 + } + """ .trimIndent() } } diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RegularExpressionMatcher.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RegularExpressionMatcher.kt index 3083e65d..664933f1 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RegularExpressionMatcher.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RegularExpressionMatcher.kt @@ -43,6 +43,7 @@ import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiT import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.applyDefaultTabComponentInsets import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.regex.RegexTextField import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.regex.SelectRegexOptionsAction import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.setContextMenu @@ -152,6 +153,8 @@ class RegularExpressionMatcher( row { cell( JBTabbedPane().apply { + applyDefaultTabComponentInsets() + addTab( UiToolsBundle.message("regular-expression-matcher.matches-title"), createMatchesTableComponent(), diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RubberDuck.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RubberDuck.kt index 87bfc4cb..9d160c5e 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RubberDuck.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RubberDuck.kt @@ -26,12 +26,14 @@ class RubberDuck(parentDisposable: Disposable) : DeveloperUiTool(parentDisposabl row { cell( JBLabel( - """ - Rubber duck debugging is a problem-solving technique where a programmer explains their code line by - line to a rubber duck or any other inanimate object. The act of explaining the code helps - the programmer to identify errors and logic mistakes in their code. This technique is widely - used in software development to improve code quality and debugging efficiency. - """ + """ + | + | Rubber duck debugging is a problem-solving technique where a programmer explains their code line by + | line to a rubber duck or any other inanimate object. The act of explaining the code helps + | the programmer to identify errors and logic mistakes in their code. This technique is widely + | used in software development to improve code quality and debugging efficiency. + | + """ .trimMargin() ) ) diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextStatistic.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextStatistic.kt index 419593eb..2c1d430e 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextStatistic.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextStatistic.kt @@ -20,6 +20,7 @@ import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEd import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.SimpleTable import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils.simpleColumnInfo +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.applyDefaultTabComponentInsets import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolContext import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolHandler import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolReference @@ -106,6 +107,8 @@ class TextStatistic( row { cell( JBTabbedPane().apply { + applyDefaultTabComponentInsets() + metricsTable = createMetricsTable() addTab("Metrics", ScrollPaneFactory.createScrollPane(metricsTable, false)) @@ -259,12 +262,12 @@ class TextStatistic( private val TEXT_EXAMPLE = """ -Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. + Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. -Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. + Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. -A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. - """ + A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. + """ .trimIndent() val openTextStatisticReference = diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/JsonPathTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/JsonPathTransformer.kt index 30956d24..4832bf87 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/JsonPathTransformer.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/JsonPathTransformer.kt @@ -268,7 +268,7 @@ class JsonPathTransformer( ${'$'}..movie[-1:].directorSelects the director of the last movie in all sub-objects of the root. - """ + """ .trimIndent() ) .copyable() diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextFilterTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextFilterTransformer.kt index 617e5840..7f392f06 100644 --- a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextFilterTransformer.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextFilterTransformer.kt @@ -233,7 +233,7 @@ class TextFilterTransformer( """ [info] Application started [error] Error occurred while processing request - """ + """ .trimIndent() } } diff --git a/modules/tools/ui/src/main/resources/message/UiToolsBundle.properties b/modules/tools/ui/src/main/resources/message/UiToolsBundle.properties index 637cbb91..368678ae 100644 --- a/modules/tools/ui/src/main/resources/message/UiToolsBundle.properties +++ b/modules/tools/ui/src/main/resources/message/UiToolsBundle.properties @@ -8,6 +8,59 @@ ascii-art.download-additional-ascii-art-fonts=Download ASCII Art Fonts From GitH ascii-art.download-additional-ascii-art-fonts-help=Download additional ASCII font files from the xero/figlet-fonts GitHub repository.

Some font files may not work correctly and will be filtered out.

These additional font files are not part of this plugin and are subject to individual licences. ascii-art.download-additional-ascii-art-fonts-failed-title=Download Files From GitHub ascii-art.download-additional-ascii-art-fonts-failed-details=Not all files could be downloaded. See idea.log for more details. +http-server.menu-title=HTTP Server +http-server.content-title=HTTP Server +http-server.download.description=This tool uses WireMock to create an easily configurable HTTP server. The necessary WireMock dependencies are not bundled with the plugin and will be downloaded from Maven Central. +http-server.download.maven-central-link=WireMock Standalon on Maven Central +http-server.download.button=Download WireMock Dependencies +http-server.download.task-title=Downloading WireMock Dependencies +http-server.download.failed-title=Download WireMock Dependencies +http-server.download.failed=Failed to download WireMock dependencies: {0} +http-server.download.missing-start-blocked=WireMock dependencies have not been downloaded yet. +http-server.server.status.label=Status: +http-server.server.status.stopped=Stopped +http-server.server.status.starting=Starting... +http-server.server.status.running=Running +http-server.server.status.restarting=Restarting... +http-server.server.status.stopping=Stopping... +http-server.server.status.stopped.exit-code=Stopped unexpectedly (exit code {0}) +http-server.server.port.label=Port: +http-server.server.advanced-config=Advanced Config +http-server.server.verbose-logging=Enable verbose WireMock logging +http-server.server.print-all-network-traffic=Print all raw network traffic +http-server.server.start=Start +http-server.server.restart=Restart +http-server.server.stop=Stop +http-server.server.restart-required=Server needs to be restarted after config change +http-server.server.start.failed-title=Start WireMock +http-server.server.start.failed=Failed to start WireMock: {0} +http-server.server.restart.failed-title=Restart WireMock +http-server.server.restart.failed=Failed to restart WireMock: {0} +http-server.server.stop.failed-title=Stop WireMock +http-server.server.stop.failed=Failed to stop WireMock: {0} +http-server.server.stop.timeout=WireMock did not stop in time. +http-server.server.start.process-exited=WireMock exited during startup with exit code {0}. {1} +http-server.server.process-output.empty=No process output was captured. +http-server.server.auto-reload-info=WireMock automatically reloads mapping file changes while the server is running. The built-in server also regenerates mappings when the inputs below change. +http-server.tab.configuration=Config +http-server.tab.output=Output +http-server.output.clear=Clear output +http-server.configuration.mode.custom-directory=Custom directory: +http-server.configuration.mode.built-in-server=Built-in server +http-server.configuration.custom-directory.title=Select WireMock Root Directory +http-server.configuration.custom-directory.missing=Select a custom directory before starting http-server. +http-server.configuration.custom-directory.invalid=The selected custom directory is invalid. +http-server.configuration.built-in-server.directory=Open built-in server directory +http-server.configuration.mappings.documentation=WireMock stub documentation (see JSON API) +http-server.advanced-config.title=Advanced Config +http-server.advanced-config.command-line-options=Additional command line options (one option per line): +http-server.advanced-config.documentation=WireMock command line options documentation +http-server.advanced-config.java-executable=Java executable: +http-server.advanced-config.java-executable.built-in-jre=Built-in JRE +http-server.advanced-config.java-executable.path=Path: +http-server.advanced-config.java-executable.path.title=Select Java executable +http-server.advanced-config.java-executable.path.missing=Select a Java executable before starting the server. +http-server.advanced-config.java-executable.path.invalid=The selected Java executable path is invalid. server-certificates.menu-title=Server Certificates server-certificates.content-title=Server Certificates server-certificates.url=URL: @@ -173,4 +226,99 @@ cron-expression.field-constraints.special.no-specific-value=?: No s cron-expression.field-constraints.special.asterix=*: Every possible value. For example: every minute, every hour, etc. cron-expression.field-constraints.description=Allowed characters:
    {0}
cron-expression.field-formats=Field formats: Single value (e.g., 5); List (e.g., 1,5,10); Range (e.g., 1-5); Step values (e.g., */5) - +jwt-encoder-decoder.menu-title=JSON Web Token (JWT) +jwt-encoder-decoder.content-title=JSON Web Token (JWT) +jwt-encoder-decoder.tab.decode-encode=Decode / Encode +jwt-encoder-decoder.tab.validate=Validate +jwt-encoder-decoder.button.decode=\u25bc Decode +jwt-encoder-decoder.button.encode=\u25b2 Encode +jwt-encoder-decoder.signature-configuration.title=Signature Algorithm Configuration +jwt-encoder-decoder.signature-configuration.algorithm=Algorithm: +jwt-encoder-decoder.signature-configuration.secret-key=Secret key: +jwt-encoder-decoder.signature-configuration.encoding=Encoding +jwt-encoder-decoder.signature-configuration.private-key=Private key: +jwt-encoder-decoder.strict-key-validation=Strict key requirements validation +jwt-encoder-decoder.strict-key-validation.help=The RFC 7518 for the JSON Web Algorithms (JWA) specifies some restrictions that a key or secret should fulfill for the computation of a signature (e.g., a minimum length). This option can be used to enforce these restrictions. +jwt-encoder-decoder.validation.key-source=Key source: +jwt-encoder-decoder.validation.public-key-or-jwk=Public key or JWK: +jwt-encoder-decoder.validation.jwks-url=JWKS URL: +jwt-encoder-decoder.validation.fetch=Fetch +jwt-encoder-decoder.validation.jwks-json=JWKS JSON: +jwt-encoder-decoder.editor.header=Header +jwt-encoder-decoder.editor.payload=Payload +jwt-encoder-decoder.editor.jwt-input=JWT input: +jwt-encoder-decoder.editor.jwt-input-output=JWT input/output: +jwt-encoder-decoder.validation.token-metadata.default=Validation uses the compact JWT shown on the left. +jwt-encoder-decoder.validation.fetch-jwks.in-progress=Fetching JWKS... +jwt-encoder-decoder.validation.fetch-jwks.in-progress-title=Fetching JWKS +jwt-encoder-decoder.validation.fetch-jwks.enter-url=Enter a JWKS URL before fetching. +jwt-encoder-decoder.validation.fetch-jwks.failed=Failed to fetch JWKS: {0} +jwt-encoder-decoder.validation.token-metadata.unknown=Unable to determine token metadata. +jwt-encoder-decoder.validation.invalid-compact-jwt.header-payload=Invalid compact JWT. Expected at least header and payload. +jwt-encoder-decoder.validation.missing-or-unsupported-alg-header=Missing or unsupported ''alg'' header value. +jwt-encoder-decoder.validation.token-metadata.algorithm=Algorithm: {0} +jwt-encoder-decoder.validation.token-metadata.kid-suffix= | kid: {0} +jwt-encoder-decoder.validation.valid-unsecured-jwt=Valid unsecured JWT (alg=none). +jwt-encoder-decoder.validation.invalid-jwt.alg-none-signature=Invalid JWT. Tokens with alg=none must not contain a signature. +jwt-encoder-decoder.validation.invalid-compact-jwt.signature=Invalid compact JWT. Signed tokens must contain a signature part. +jwt-encoder-decoder.validation.failed=Validation failed: {0} +jwt-encoder-decoder.validation.key-source-mismatch.public-key-or-jwks=Key source mismatch. {0} requires a public key or JWKS. +jwt-encoder-decoder.validation.signature.valid=Signature valid. +jwt-encoder-decoder.validation.signature.invalid=Signature invalid. +jwt-encoder-decoder.validation.key-source-mismatch.shared-secret=Key source mismatch. {0} requires a shared secret. +jwt-encoder-decoder.validation.provide-public-key-or-jwk=Provide a public key or JWK. +jwt-encoder-decoder.validation.read-public-key-or-jwk.failed=Failed to read public key or JWK: {0} +jwt-encoder-decoder.validation.provide-jwks-json=Provide JWKS JSON or fetch it from the configured JWKS URL. +jwt-encoder-decoder.validation.parse-jwks.failed=Failed to parse JWKS: {0} +jwt-encoder-decoder.validation.no-compatible-key-with-kid=No compatible key with kid ''{0}'' found in the JWKS. +jwt-encoder-decoder.validation.no-compatible-key=No compatible verification key found in the JWKS. +jwt-encoder-decoder.validation.signature.valid-using-key=Signature valid using key ''{0}''. +jwt-encoder-decoder.signature-algorithm.none=None +jwt-encoder-decoder.signature-algorithm.named={0} ({1}) +jwt-encoder-decoder.secret-key-encoding-mode.raw=Raw +jwt-encoder-decoder.secret-key-encoding-mode.base32=Base32 Encoded +jwt-encoder-decoder.secret-key-encoding-mode.base64=Base64 Encoded +jwt-encoder-decoder.validation-key-source.secret=Shared Secret +jwt-encoder-decoder.validation-key-source.public-key=Public Key / JWK +jwt-encoder-decoder.validation-key-source.jwks=JWKS +jwt-encoder-decoder.standard-claim.tooltip={0} ({1})
{2} +jwt-encoder-decoder.standard-claim.typ.title=Type +jwt-encoder-decoder.standard-claim.typ.description=Indicating that this token is a JWT. +jwt-encoder-decoder.standard-claim.iss.title=Issuer +jwt-encoder-decoder.standard-claim.iss.description=The issuer of the JWT. +jwt-encoder-decoder.standard-claim.sub.title=Subject +jwt-encoder-decoder.standard-claim.sub.description=The subject of the JWT. +jwt-encoder-decoder.standard-claim.aud.title=Audience +jwt-encoder-decoder.standard-claim.aud.description=The recipient of the JWT. +jwt-encoder-decoder.standard-claim.exp.title=Expiration Time +jwt-encoder-decoder.standard-claim.exp.description=JWT expiration time. +jwt-encoder-decoder.standard-claim.nbf.title=Not Before Time +jwt-encoder-decoder.standard-claim.nbf.description=JWT valid after this time. +jwt-encoder-decoder.standard-claim.iat.title=Issued at Time +jwt-encoder-decoder.standard-claim.iat.description=JWT issued at this time. +jwt-encoder-decoder.standard-claim.jti.title=JWT ID +jwt-encoder-decoder.standard-claim.jti.description=A unique identifier for the JWT. +jwt-encoder-decoder.standard-claim.alg.title=Algorithm +jwt-encoder-decoder.standard-claim.alg.description=The algorithm to calculate the signature of this JWT. +jwt-encoder-decoder.standard-claim.azp.title=Authorized Party +jwt-encoder-decoder.standard-claim.azp.description=The party to which the JWT was issued. +jwt-encoder-decoder.standard-claim.sid.title=Session ID +jwt-encoder-decoder.standard-claim.sid.description=An unique session ID. +jwt-encoder-decoder.standard-claim.nonce.title=Nonce +jwt-encoder-decoder.standard-claim.nonce.description=A value used to associate a client session with this JWT. +jwt-encoder-decoder.standard-claim.at-hash.title=Access Token Hash Value +jwt-encoder-decoder.standard-claim.at-hash.description=The hash of an access token. +jwt-encoder-decoder.standard-claim.c-hash.title=Code Hash Value +jwt-encoder-decoder.standard-claim.c-hash.description=The hash of a code. +jwt-encoder-decoder.standard-claim.act.title=Actor +jwt-encoder-decoder.standard-claim.act.description=The hash of an access token. +jwt-encoder-decoder.standard-claim.auth-time.title=Authentication Time +jwt-encoder-decoder.standard-claim.auth-time.description=Time of user authentication. +jwt-encoder-decoder.standard-claim.scope.title=Scope +jwt-encoder-decoder.standard-claim.scope.description=Permissions granted to the token. +jwt-encoder-decoder.encode.signature-unavailable.header-payload-errors=Unable to compute signature due to header or payload errors +jwt-encoder-decoder.encode.signature-unavailable.configuration-errors=Unable to compute signature due signature configuration errors +jwt-encoder-decoder.header.unsupported-algorithm=Unsupported algorithm: ''{0}'' +jwt-encoder-decoder.header.missing-algorithm=Missing algorithm header field: ''alg'' +jwt-encoder-decoder.signature.compute-failed=Failed to compute signature: +jwt-encoder-decoder.signature.private-key-required=A private key must be provided diff --git a/modules/tools/ui/src/main/resources/message/UiToolsBundle_de.properties b/modules/tools/ui/src/main/resources/message/UiToolsBundle_de.properties index 0f791564..356ab64d 100644 --- a/modules/tools/ui/src/main/resources/message/UiToolsBundle_de.properties +++ b/modules/tools/ui/src/main/resources/message/UiToolsBundle_de.properties @@ -8,6 +8,59 @@ ascii-art.download-additional-ascii-art-fonts=ASCII-Art-Schriftarten von GitHub ascii-art.download-additional-ascii-art-fonts-help=Zus\u00e4tzliche ASCII-Schriftartdateien aus dem xero/figlet-fonts-GitHub-Repository herunterladen.

Einige Schriftartdateien funktionieren m\u00f6glicherweise nicht korrekt und werden herausgefiltert.

Diese zus\u00e4tzlichen Schriftartdateien sind nicht Teil dieses Plugins und unterliegen individuellen Lizenzen. ascii-art.download-additional-ascii-art-fonts-failed-title=GitHub-Dateien Herunterladen ascii-art.download-additional-ascii-art-fonts-failed-details=Nicht alle Dateien konnten heruntergeladen werden. Weitere Details finden Sie in dem idea.log. +http-server.menu-title=HTTP Server +http-server.content-title=HTTP Server +http-server.download.description=Dieses Werkzeug verwendet WireMock, um einen einfach konfigurierbaren HTTP-Server zu erstellen. Die erforderlichen WireMock-Abh\u00e4ngigkeiten sind nicht im Plugin enthalten und werden von Maven Central heruntergeladen. +http-server.download.maven-central-link=WireMock Standalon on Maven Central +http-server.download.button=WireMock-Abh\u00e4ngigkeiten herunterladen +http-server.download.task-title=WireMock-Abh\u00e4ngigkeiten werden heruntergeladen +http-server.download.failed-title=WireMock-Abh\u00e4ngigkeiten herunterladen +http-server.download.failed=Fehler beim Herunterladen der WireMock-Abh\u00e4ngigkeiten: {0} +http-server.download.missing-start-blocked=WireMock-Abh\u00e4ngigkeiten wurden noch nicht heruntergeladen. +http-server.server.status.label=Status: +http-server.server.status.stopped=Gestoppt +http-server.server.status.starting=Startet... +http-server.server.status.running=L\u00e4uft +http-server.server.status.restarting=Wird neu gestartet... +http-server.server.status.stopping=Wird gestoppt... +http-server.server.status.stopped.exit-code=Unerwartet gestoppt (Exit-Code {0}) +http-server.server.port.label=Port: +http-server.server.advanced-config=Erweiterte Konfiguration +http-server.server.verbose-logging=Ausführliche WireMock-Protokollierung aktivieren +http-server.server.print-all-network-traffic=Gesamten rohen Netzwerkverkehr ausgeben +http-server.server.start=Starten +http-server.server.restart=Neu starten +http-server.server.stop=Stoppen +http-server.server.restart-required=Der Server muss nach einer Konfigurations\u00e4nderung neu gestartet werden +http-server.server.start.failed-title=WireMock starten +http-server.server.start.failed=Fehler beim Starten von WireMock: {0} +http-server.server.restart.failed-title=WireMock neu starten +http-server.server.restart.failed=Fehler beim Neustarten von WireMock: {0} +http-server.server.stop.failed-title=WireMock stoppen +http-server.server.stop.failed=Fehler beim Stoppen von WireMock: {0} +http-server.server.stop.timeout=WireMock konnte nicht rechtzeitig gestoppt werden. +http-server.server.start.process-exited=WireMock wurde beim Start mit Exit-Code {0} beendet. {1} +http-server.server.process-output.empty=Es wurde keine Prozessausgabe erfasst. +http-server.server.auto-reload-info=WireMock l\u00e4dt \u00c4nderungen an Mapping-Dateien automatisch neu, w\u00e4hrend der Server l\u00e4uft. Der integrierte Server erzeugt Mappings zus\u00e4tzlich neu, wenn sich die Eingaben unten \u00e4ndern. +http-server.tab.configuration=Konfiguration +http-server.tab.output=Ausgabe +http-server.output.clear=Ausgabe löschen +http-server.configuration.mode.custom-directory=Benutzerdefiniertes Verzeichnis: +http-server.configuration.mode.built-in-server=Integrierter Server +http-server.configuration.custom-directory.title=WireMock-Root-Verzeichnis ausw\u00e4hlen +http-server.configuration.custom-directory.missing=W\u00e4hlen Sie vor dem Starten von WireMock ein benutzerdefiniertes Verzeichnis aus. +http-server.configuration.custom-directory.invalid=Das ausgew\u00e4hlte benutzerdefinierte Verzeichnis ist ung\u00fcltig. +http-server.configuration.built-in-server.directory=Verzeichnis des integrierten Servers \u00f6ffnen +http-server.configuration.mappings.documentation=WireMock-Dokumentation zu Stubs (siehe JSON API) +http-server.advanced-config.title=Erweiterte Konfiguration +http-server.advanced-config.command-line-options=Zus\u00e4tzliche Kommandozeilenoptionen (eine Option pro Zeile): +http-server.advanced-config.documentation=WireMock-Dokumentation der Kommandozeilenoptionen +http-server.advanced-config.java-executable=Java-Executable: +http-server.advanced-config.java-executable.built-in-jre=Integrierte JRE +http-server.advanced-config.java-executable.path=Pfad: +http-server.advanced-config.java-executable.path.title=Java-Executable ausw\u00e4hlen +http-server.advanced-config.java-executable.path.missing=W\u00e4hlen Sie vor dem Starten des Servers ein Java-Executable aus. +http-server.advanced-config.java-executable.path.invalid=Der ausgew\u00e4hlte Pfad zum Java-Executable ist ung\u00fcltig. server-certificates.menu-title=Server-Zertifikate server-certificates.content-title=Server-Zertifikate server-certificates.url=URL: @@ -94,7 +147,7 @@ escaper-unescaper.to-target-title=Escapen escaper-unescaper.to-source-title=Unescapen escape-sequences-escaper-unescaper.title=Escape-Sequenzen escape-sequences-escaper-unescaper.content-title=Escape-Sequenzen Escaper/Unescaper -escape-sequences-escaper-unescaper.context-help=Escapes oder unescapes Zeilenumbrche, Tabulatoren, Rckwrtsschrgstriche, einfache Anfhrungszeichen und doppelte Anfhrungszeichen. +escape-sequences-escaper-unescaper.context-help=Escapes oder unescapes Zeilenumbrüche, Tabulatoren, Rückwärtsschrägstriche, einfache Anführungszeichen und doppelte Anführungszeichen. escape-sequences-escaper-unescaper.do-line-break-decoding=Zeilenumbr\u00fcche: escape-sequences-escaper-unescaper.do-tab-decoding=Tabulatoren (\\t) escape-sequences-escaper-unescaper.do-backslash-decoding=Backslashes (\\\) @@ -139,9 +192,9 @@ color-picker.title=Farbw\u00e4hler color-picker.content-title=Farbw\u00e4hler cron-expression.menu-title=Cron Expression cron-expression.content-title=Cron Expression -cron-expression.cron-next-execution=Nchste Ausfhrung: -cron-expression.cron-last-execution=Letzte Ausfhrung: -cron-expression.cron-explanation=Erklrung: +cron-expression.cron-next-execution=Nächste Ausführung: +cron-expression.cron-last-execution=Letzte Ausführung: +cron-expression.cron-explanation=Erklärung: cron-expression.unknown=Unbekannt cron-expression.cron-type=Typ: cron-expression.cron-fields.input-second=Sekunde: @@ -167,9 +220,105 @@ cron-expression.field-constraints.named-month-values=Benannte Monate von J cron-expression.field-constraints.special.last-weekday=LW: Letzter Werktag des Monats cron-expression.field-constraints.special.last-day-of-month=L: Letzter Tag des Monats cron-expression.field-constraints.special.last-occurrence-of-a-weekday=nL: Letztes Vorkommen des Wochentags n (z.B. 5L = letzter Freitag) -cron-expression.field-constraints.special.nearest-weekday=W: Nchstgelegener Werktag zum angegebenen Tag +cron-expression.field-constraints.special.nearest-weekday=W: Nächstgelegener Werktag zum angegebenen Tag cron-expression.field-constraints.special.weekday-month=n#m: m-te Vorkommen des Wochentags n im Monat (z.B. 2#3 = 3. Montag des Monats) cron-expression.field-constraints.special.no-specific-value=?: Kein bestimmter Wert -cron-expression.field-constraints.special.asterix=*: Jeden mglichen Wert. Z.B. jede Minute, jede Stunde usw. +cron-expression.field-constraints.special.asterix=*: Jeden möglichen Wert. Z.B. jede Minute, jede Stunde usw. cron-expression.field-constraints.description=Erlaubte Zeichen:
    {0}
cron-expression.field-formats=Feldformate: einzelner Wert (z.B. 5); Liste (z.B. 1,5,10); Bereich (z.B. 1-5); Schrittweite (z.B. */5) +jwt-encoder-decoder.menu-title=JSON Web Token (JWT) +jwt-encoder-decoder.content-title=JSON Web Token (JWT) +jwt-encoder-decoder.tab.decode-encode=Dekodieren / Kodieren +jwt-encoder-decoder.tab.validate=Validieren +jwt-encoder-decoder.button.decode=\u25bc Dekodieren +jwt-encoder-decoder.button.encode=\u25b2 Kodieren +jwt-encoder-decoder.signature-configuration.title=Signaturalgorithmus-Konfiguration +jwt-encoder-decoder.signature-configuration.algorithm=Algorithmus: +jwt-encoder-decoder.signature-configuration.secret-key=Geheimer Schl\u00fcssel: +jwt-encoder-decoder.signature-configuration.encoding=Kodierung +jwt-encoder-decoder.signature-configuration.private-key=Privater Schl\u00fcssel: +jwt-encoder-decoder.strict-key-validation=Strikte Validierung der Schl\u00fcsselanforderungen +jwt-encoder-decoder.strict-key-validation.help=Die RFC 7518 f\u00fcr JSON Web Algorithms (JWA) beschreibt einige Einschr\u00e4nkungen, die ein Schl\u00fcssel oder Geheimnis f\u00fcr die Berechnung einer Signatur erf\u00fcllen sollte, zum Beispiel eine Mindestl\u00e4nge. Mit dieser Option lassen sich diese Einschr\u00e4nkungen erzwingen. +jwt-encoder-decoder.validation.key-source=Schl\u00fcsselquelle: +jwt-encoder-decoder.validation.public-key-or-jwk=\u00d6ffentlicher Schl\u00fcssel oder JWK: +jwt-encoder-decoder.validation.jwks-url=JWKS-URL: +jwt-encoder-decoder.validation.fetch=Abrufen +jwt-encoder-decoder.validation.jwks-json=JWKS-JSON: +jwt-encoder-decoder.editor.header=Header +jwt-encoder-decoder.editor.payload=Payload +jwt-encoder-decoder.editor.jwt-input=JWT-Eingabe: +jwt-encoder-decoder.editor.jwt-input-output=JWT-Eingabe/Ausgabe: +jwt-encoder-decoder.validation.token-metadata.default=Zur Validierung wird das kompakte JWT auf der linken Seite verwendet. +jwt-encoder-decoder.validation.fetch-jwks.in-progress=JWKS wird abgerufen... +jwt-encoder-decoder.validation.fetch-jwks.in-progress-title=JWKS wird abgerufen +jwt-encoder-decoder.validation.fetch-jwks.enter-url=Geben Sie vor dem Abrufen eine JWKS-URL ein. +jwt-encoder-decoder.validation.fetch-jwks.failed=JWKS konnte nicht abgerufen werden: {0} +jwt-encoder-decoder.validation.token-metadata.unknown=Token-Metadaten konnten nicht ermittelt werden. +jwt-encoder-decoder.validation.invalid-compact-jwt.header-payload=Ung\u00fcltiges kompaktes JWT. Mindestens Header und Payload werden erwartet. +jwt-encoder-decoder.validation.missing-or-unsupported-alg-header=Fehlender oder nicht unterst\u00fctzter ''alg''-Headerwert. +jwt-encoder-decoder.validation.token-metadata.algorithm=Algorithmus: {0} +jwt-encoder-decoder.validation.token-metadata.kid-suffix= | kid: {0} +jwt-encoder-decoder.validation.valid-unsecured-jwt=G\u00fcltiges ungesichertes JWT (alg=none). +jwt-encoder-decoder.validation.invalid-jwt.alg-none-signature=Ung\u00fcltiges JWT. Tokens mit alg=none d\u00fcrfen keine Signatur enthalten. +jwt-encoder-decoder.validation.invalid-compact-jwt.signature=Ung\u00fcltiges kompaktes JWT. Signierte Tokens m\u00fcssen einen Signaturteil enthalten. +jwt-encoder-decoder.validation.failed=Validierung fehlgeschlagen: {0} +jwt-encoder-decoder.validation.key-source-mismatch.public-key-or-jwks=Nicht passende Schl\u00fcsselquelle. {0} erfordert einen \u00f6ffentlichen Schl\u00fcssel oder JWKS. +jwt-encoder-decoder.validation.signature.valid=Signatur ist g\u00fcltig. +jwt-encoder-decoder.validation.signature.invalid=Signatur ist ung\u00fcltig. +jwt-encoder-decoder.validation.key-source-mismatch.shared-secret=Nicht passende Schl\u00fcsselquelle. {0} erfordert ein gemeinsames Geheimnis. +jwt-encoder-decoder.validation.provide-public-key-or-jwk=Geben Sie einen \u00f6ffentlichen Schl\u00fcssel oder JWK an. +jwt-encoder-decoder.validation.read-public-key-or-jwk.failed=\u00d6ffentlicher Schl\u00fcssel oder JWK konnte nicht gelesen werden: {0} +jwt-encoder-decoder.validation.provide-jwks-json=Geben Sie JWKS-JSON an oder rufen Sie es von der konfigurierten JWKS-URL ab. +jwt-encoder-decoder.validation.parse-jwks.failed=JWKS konnte nicht geparst werden: {0} +jwt-encoder-decoder.validation.no-compatible-key-with-kid=Kein kompatibler Schl\u00fcssel mit kid ''{0}'' wurde im JWKS gefunden. +jwt-encoder-decoder.validation.no-compatible-key=Kein kompatibler Verifizierungsschl\u00fcssel wurde im JWKS gefunden. +jwt-encoder-decoder.validation.signature.valid-using-key=Signatur ist mit Schl\u00fcssel ''{0}'' g\u00fcltig. +jwt-encoder-decoder.signature-algorithm.none=Kein +jwt-encoder-decoder.signature-algorithm.named={0} ({1}) +jwt-encoder-decoder.secret-key-encoding-mode.raw=Roh +jwt-encoder-decoder.secret-key-encoding-mode.base32=Base32-kodiert +jwt-encoder-decoder.secret-key-encoding-mode.base64=Base64-kodiert +jwt-encoder-decoder.validation-key-source.secret=Gemeinsames Geheimnis +jwt-encoder-decoder.validation-key-source.public-key=\u00d6ffentlicher Schl\u00fcssel / JWK +jwt-encoder-decoder.validation-key-source.jwks=JWKS +jwt-encoder-decoder.standard-claim.tooltip={0} ({1})
{2} +jwt-encoder-decoder.standard-claim.typ.title=Typ +jwt-encoder-decoder.standard-claim.typ.description=Zeigt an, dass dieses Token ein JWT ist. +jwt-encoder-decoder.standard-claim.iss.title=Aussteller +jwt-encoder-decoder.standard-claim.iss.description=Der Aussteller des JWT. +jwt-encoder-decoder.standard-claim.sub.title=Subjekt +jwt-encoder-decoder.standard-claim.sub.description=Das Subjekt des JWT. +jwt-encoder-decoder.standard-claim.aud.title=Empf\u00e4nger +jwt-encoder-decoder.standard-claim.aud.description=Der Empf\u00e4nger des JWT. +jwt-encoder-decoder.standard-claim.exp.title=Ablaufzeit +jwt-encoder-decoder.standard-claim.exp.description=Ablaufzeit des JWT. +jwt-encoder-decoder.standard-claim.nbf.title=G\u00fcltig ab +jwt-encoder-decoder.standard-claim.nbf.description=JWT ist nach diesem Zeitpunkt g\u00fcltig. +jwt-encoder-decoder.standard-claim.iat.title=Ausstellungszeit +jwt-encoder-decoder.standard-claim.iat.description=JWT wurde zu diesem Zeitpunkt ausgestellt. +jwt-encoder-decoder.standard-claim.jti.title=JWT-ID +jwt-encoder-decoder.standard-claim.jti.description=Eine eindeutige Kennung f\u00fcr das JWT. +jwt-encoder-decoder.standard-claim.alg.title=Algorithmus +jwt-encoder-decoder.standard-claim.alg.description=Der Algorithmus zur Berechnung der Signatur dieses JWT. +jwt-encoder-decoder.standard-claim.azp.title=Autorisierte Partei +jwt-encoder-decoder.standard-claim.azp.description=Die Partei, f\u00fcr die das JWT ausgestellt wurde. +jwt-encoder-decoder.standard-claim.sid.title=Sitzungs-ID +jwt-encoder-decoder.standard-claim.sid.description=Eine eindeutige Sitzungs-ID. +jwt-encoder-decoder.standard-claim.nonce.title=Nonce +jwt-encoder-decoder.standard-claim.nonce.description=Ein Wert, der eine Client-Sitzung mit diesem JWT verkn\u00fcpft. +jwt-encoder-decoder.standard-claim.at-hash.title=Access-Token-Hashwert +jwt-encoder-decoder.standard-claim.at-hash.description=Der Hash eines Access Tokens. +jwt-encoder-decoder.standard-claim.c-hash.title=Code-Hashwert +jwt-encoder-decoder.standard-claim.c-hash.description=Der Hash eines Codes. +jwt-encoder-decoder.standard-claim.act.title=Akteur +jwt-encoder-decoder.standard-claim.act.description=Der Hash eines Access Tokens. +jwt-encoder-decoder.standard-claim.auth-time.title=Authentifizierungszeit +jwt-encoder-decoder.standard-claim.auth-time.description=Zeitpunkt der Benutzer-Authentifizierung. +jwt-encoder-decoder.standard-claim.scope.title=Berechtigungsumfang +jwt-encoder-decoder.standard-claim.scope.description=Dem Token gew\u00e4hrte Berechtigungen. +jwt-encoder-decoder.encode.signature-unavailable.header-payload-errors=Signatur kann aufgrund von Header- oder Payload-Fehlern nicht berechnet werden +jwt-encoder-decoder.encode.signature-unavailable.configuration-errors=Signatur kann aufgrund von Fehlern in der Signaturkonfiguration nicht berechnet werden +jwt-encoder-decoder.header.unsupported-algorithm=Nicht unterst\u00fctzter Algorithmus: ''{0}'' +jwt-encoder-decoder.header.missing-algorithm=Fehlendes Algorithmus-Header-Feld: ''alg'' +jwt-encoder-decoder.signature.compute-failed=Signatur konnte nicht berechnet werden: +jwt-encoder-decoder.signature.private-key-required=Ein privater Schl\u00fcssel muss angegeben werden diff --git a/modules/tools/ui/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPaneTest.kt b/modules/tools/ui/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPaneTest.kt new file mode 100644 index 00000000..428c2a20 --- /dev/null +++ b/modules/tools/ui/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPaneTest.kt @@ -0,0 +1,29 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common + +import com.intellij.openapi.ui.getUserData +import com.intellij.openapi.ui.putUserData +import com.intellij.openapi.util.Key +import com.intellij.testFramework.junit5.RunMethodInEdt +import javax.swing.JPanel +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class TitledTabbedPaneTest { + @Test + @RunMethodInEdt(writeIntent = RunMethodInEdt.WriteIntentMode.True) + fun `on selection changed passes the original wrapped tab content`() { + val selectionKey = Key.create("selectionKey") + val firstTab = JPanel().apply { putUserData(selectionKey, "first") } + val secondTab = JPanel().apply { putUserData(selectionKey, "second") } + val selections = mutableListOf() + + val titledTabbedPane = + TitledTabbedPane("Title", listOf("First" to firstTab, "Second" to secondTab)).apply { + onSelectionChanged { selections.add(checkNotNull(it.getUserData(selectionKey))) } + } + + titledTabbedPane.selectedIndex = 2 + + assertThat(selections).containsExactly("second") + } +} diff --git a/modules/tools/ui/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ExternalSystemProcessRegistryTest.kt b/modules/tools/ui/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ExternalSystemProcessRegistryTest.kt new file mode 100644 index 00000000..d794769c --- /dev/null +++ b/modules/tools/ui/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ExternalSystemProcessRegistryTest.kt @@ -0,0 +1,84 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.intellij.execution.process.ProcessHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures.IdeaTest +import java.io.ByteArrayOutputStream +import java.io.OutputStream +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ExternalSystemProcessRegistryTest : IdeaTest() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + @Test + fun `stops project processes when a project is disposed`() { + val registry = ExternalSystemProcessRegistry() + val projectProcess = TestProcess() + val applicationProcess = TestProcess() + + registry.register(fixture.project, projectProcess) + registry.register(null, applicationProcess) + + registry.stopProcesses(fixture.project) + + assertThat(projectProcess.destroyProcessCalls).isEqualTo(1) + assertThat(projectProcess.isProcessTerminated).isTrue() + assertThat(applicationProcess.destroyProcessCalls).isZero() + assertThat(applicationProcess.isProcessTerminated).isFalse() + } + + @Test + fun `does not stop unregistered processes on shutdown`() { + val registry = ExternalSystemProcessRegistry() + val process = TestProcess() + + registry.register(fixture.project, process) + registry.unregister(process) + registry.dispose() + + assertThat(process.destroyProcessCalls).isZero() + assertThat(process.isProcessTerminated).isFalse() + } + + @Test + fun `stops remaining processes on application shutdown`() { + val registry = ExternalSystemProcessRegistry() + val process = TestProcess() + + registry.register(null, process) + registry.dispose() + + assertThat(process.destroyProcessCalls).isEqualTo(1) + assertThat(process.isProcessTerminated).isTrue() + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + private class TestProcess : ProcessHandler() { + + var destroyProcessCalls = 0 + private set + + init { + startNotify() + } + + override fun destroyProcessImpl() { + destroyProcessCalls++ + notifyProcessTerminated(0) + } + + override fun detachProcessImpl() { + notifyProcessDetached() + } + + override fun detachIsDefault(): Boolean = false + + override fun getProcessInput(): OutputStream = ByteArrayOutputStream() + } + + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolUnderTest.kt b/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolUnderTest.kt index 19f5c3c4..8780c215 100644 --- a/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolUnderTest.kt +++ b/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolUnderTest.kt @@ -11,7 +11,7 @@ import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolCon import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactoryEp -import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.jwtencoderdecoder.SignatureAlgorithm import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.HmacTransformer import java.math.BigDecimal import java.security.Security @@ -85,26 +85,25 @@ open class DeveloperUiToolUnderTest( id == "date-time-converter" && property.key == "timeZoneId" -> ZoneId.getAvailableZoneIds().random { it == property.defaultValue } - id == "jwt-encoder-decoder" && property.key == "algorithm" -> - JwtEncoderDecoder.SignatureAlgorithm.HMAC512 + id == "jwt-encoder-decoder" && property.key == "algorithm" -> SignatureAlgorithm.HMAC512 id == "jwt-encoder-decoder" && property.key == "encodedText" -> "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.ANCf_8p1AE4ZQs7QuqGAyyfTEgYrKSjKWkhBk5cIn1_2QVr2jEjmM-1tu7EgnyOf_fAsvdFXva8Sv05iTGzETg" id == "jwt-encoder-decoder" && property.key == "headerText" -> """ - { - "alg": "HS512", - "typ": "JWT" - } + { + "alg": "HS512", + "typ": "JWT" + } """ .trimIndent() id == "jwt-encoder-decoder" && property.key == "payloadText" -> """ - { - "sub": "1234567890", - "name": "John Doe", - "admin": true, - "iat": 1516239022 - } + { + "sub": "1234567890", + "name": "John Doe", + "admin": true, + "iat": 1516239022 + } """ .trimIndent() diff --git a/settings.gradle.kts b/settings.gradle.kts index 58ff4ebb..c45cfe9b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,7 +10,7 @@ val modules = mutableSetOf( Module("tools-editor", Paths.get("modules/tools/editor")), Module("tools-ui", Paths.get("modules/tools/ui")) ) -if (platform == "IC") { +if (platform == "idea") { modules.add(Module("java-dependent")) modules.add(Module("kotlin-dependent")) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 2b9b468b..4d8d61b4 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -6,53 +6,55 @@ This plugin is a powerful and versatile set of tools designed to enhance the development experience for software engineers. With its extensive collection of features, developers can increase their productivity and simplify complex operations without leaving their coding environment.

+

Developer Tools brings a practical toolbox of everyday development utilities directly into IntelliJ-based IDEs. It keeps common tasks such as encoding data, transforming text, validating JSON, generating identifiers, inspecting archives, formatting code and SQL, and checking certificates inside the IDE, so you do not need to switch to separate web tools or command-line snippets.

Plugin icon by Gabriele Malaspina.

Key Features

    -
  • Encoding and Decoding: JWT (JSON Web Tokens), Base32, Base64, URL Base64, MIME Base64, URL encoding, line breaks
  • +
  • JWT Encoder/Decoder
  • +
  • Base32, Base64, URL Base64, MIME Base64, URL, and ASCII Encoder/Decoder
  • +
  • Text escaping and unescaping for HTML entities, Java strings, JSON, CSV, XML, and escape sequences
  • Regular Expression Matcher
  • -
  • UUID, ULID, Nano ID and Password Generator
  • +
  • UUID, ULID, Nano ID, password, QR code/barcode, Lorem Ipsum, and ASCII art generators
  • Text Sorting
  • Text Case Transformation
  • Text Diff Viewer
  • Text Format Conversion
  • -
  • Text Escape: HTML entities, Java Strings, JSON, CSV, and XML
  • +
  • Text Statistic
  • Text Filter
  • JSON Path Parser
  • JSON Schema Validator
  • -
  • Hashing
  • -
  • Archive (ZIP, TAR, JAR, 7z, ...) viewer and extractor
  • -
  • Date Time Handling (Unix Timestamp, Formatting, ...)
  • -
  • Units converters for time, data size and transfer rate
  • +
  • Hashing and HMAC
  • +
  • HTTP Server (WireMock)
  • +
  • Archive viewer and extractor for ZIP, TAR, JAR, 7z, and other formats
  • +
  • Date and time tools for Unix timestamps, formatting, and parsing
  • +
  • Unit converters for time, data size, and transfer rate
  • Code Style Formatting
  • SQL Formatting
  • +
  • CLI Command Conversion
  • Color Picker
  • -
  • Server certificates fetching, analyse and export
  • -
  • QR Code/Barcode Generator
  • -
  • Lorem Ipsum Generator
  • -
  • ASCII Art
  • +
  • Fetching, analyzing, and exporting server certificates
  • +
  • Notes

Integration

-

The main tools are currently available as a standalone dialog or tool window. Additionally, some tools are also available via the editor menu or code intentions. Some of these tools are only available if a text is selected, or the current caret position is on a Java/Kotlin string or identifier.

+

The full toolbox is available in both a persistent tool window and a standalone dialog. Tools can have multiple named workbenches, so you can keep separate inputs and configurations for different tasks. Frequently used text operations are also available from the editor popup menu and as intentions; depending on the action, they work on selected text or on the Java/Kotlin string or identifier at the caret.

-

The plugin settings can be found in IntelliJ's settings/preferences under Tools | Developer Tools.

+

Plugin settings are available in IntelliJ IDEA's settings/preferences under Tools | Developer Tools.

Tool Window

-

The tool window is available under View | Tool Windows | Tools. All inputs and configurations will be stored in the project.

+

The tool window is available under View | Tool Windows | Developer Tools. Inputs, selected tools, expanded menu groups, and tool configuration are stored per project.

-

Dialog

+

Dialog

-

The action to access the dialog is available through IntelliJ's main menu under Tools | Developer Tools.

+

The dialog is available from IntelliJ IDEA's main menu under Tools | Developer Tools.

-

To add the "Open Dialog" action to the main toolbar, we can either enable it in IntelliJ's settings/preferences under Tools | Developer Tools, or manually add the action via Customize Toolbar... | Add Actions... | Developer Tools.

+

To add the "Open Dialog" action to the main toolbar, enable it in IntelliJ IDEA's settings/preferences under Tools | Developer Tools, or add it manually via Customize Toolbar... | Add Actions... | Developer Tools.

-

All inputs and configurations of the dialog will be stored on the application level.

+

Dialog inputs, selected tools, expanded menu groups, and tool configuration are stored at the application level.

]]>
com.intellij.modules.platform @@ -145,7 +147,7 @@ + implementationClass="dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.jwtencoderdecoder.JwtEncoderDecoder$Factory"/> + @@ -322,11 +327,11 @@ legacyId="dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter$StandardFormat"/> + + - \ No newline at end of file + diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/CliCommandConverterTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/CliCommandConverterTest.kt index a389a754..cffa8fd7 100644 --- a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/CliCommandConverterTest.kt +++ b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/CliCommandConverterTest.kt @@ -37,12 +37,12 @@ class CliCommandConverterTest { assertThat(actual) .isEqualTo( """ -app \ - -foo \ - --baz \ - ---baz \ - -foo-bar "foo -baz" '-foo-bar' - """ + app \ + -foo \ + --baz \ + ---baz \ + -foo-bar "foo -baz" '-foo-bar' + """ .trimIndent() ) } @@ -59,12 +59,12 @@ app \ val actual = cliCommandConverter.doConvertToSource( """ -app \ - -foo \ - --baz \ - ---baz \ - -foo-bar "foo -baz" '-foo-bar' - """ + app \ + -foo \ + --baz \ + ---baz \ + -foo-bar "foo -baz" '-foo-bar' + """ .trimIndent() .toByteArray() ) diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/DeveloperToolsInstanceSettingsTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/DeveloperToolsInstanceSettingsTest.kt index 03fa62d0..d148996b 100644 --- a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/DeveloperToolsInstanceSettingsTest.kt +++ b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/DeveloperToolsInstanceSettingsTest.kt @@ -21,8 +21,7 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardOpenOption -import java.util.Locale -import java.util.SortedMap +import java.util.* import kotlin.io.path.bufferedReader import kotlin.io.path.createDirectories import kotlin.io.path.exists @@ -112,7 +111,7 @@ class DeveloperToolsInstanceSettingsTest : IdeaTest() { testNodes.add( dynamicTest("No persisted properties after they have been reset") { // Load all example values - DeveloperToolsApplicationSettings.Companion.generalSettings.loadExamples.set(true) + DeveloperToolsApplicationSettings.generalSettings.loadExamples.set(true) developerUiToolsUnderTest.forEach { it.resetConfiguration(loadExamples = true) } // Expect: No configurations have persisted because there are no property changes diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/plugin/PluginXmlTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/plugin/PluginXmlTest.kt index 7c09125c..cb0cfc9a 100644 --- a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/plugin/PluginXmlTest.kt +++ b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/plugin/PluginXmlTest.kt @@ -1,6 +1,8 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.plugin import com.intellij.openapi.util.JDOMUtil +import java.nio.file.Files +import java.nio.file.Path import org.assertj.core.api.Assertions.assertThat import org.jdom.Element import org.junit.jupiter.api.BeforeAll @@ -102,8 +104,9 @@ class PluginXmlTest { @BeforeAll @JvmStatic fun beforeAll() { - pluginXml = - JDOMUtil.load(PluginXmlTest::class.java.getResourceAsStream("/META-INF/plugin.xml")) + Files.newInputStream(Path.of("src/main/resources/META-INF/plugin.xml")).use { + pluginXml = JDOMUtil.load(it) + } } } } diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/expected-configuration-properties.csv b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/expected-configuration-properties.csv new file mode 100644 index 00000000..905776f0 --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/expected-configuration-properties.csv @@ -0,0 +1,576 @@ +developerToolId,propertyKey,propertyType,propertyValueTypeName,propertyValue +ascii-art,asciiArtOutput-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ascii-art,asciiArtOutput-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-art,asciiArtOutput-editor-softWraps,CONFIGURATION,kotlin.Boolean,true +ascii-art,selectedFontFileName,CONFIGURATION,kotlin.String,slant.flf +ascii-art,textInput,INPUT,kotlin.String,6rSbxqPSLGQCiUBQ5GEh +ascii-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +ascii-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +ascii-encoder-decoder,sourceFile,INPUT,kotlin.String,JWsL4hqqGvxNP5a29uNM +ascii-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +ascii-encoder-decoder,sourceText,INPUT,kotlin.String,SwdQfNWz8mPVK8ru4rEt +ascii-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +ascii-encoder-decoder,targetFile,INPUT,kotlin.String,ITTksDJmdiNJh6MtTVZu +ascii-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +ascii-encoder-decoder,targetText,INPUT,kotlin.String,SwdQfNWz8mPVK8ru4rEt +base32-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,sourceFile,INPUT,kotlin.String,yGLu7VYYOKvFB3HSJZV2 +base32-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +base32-encoder-decoder,sourceText,INPUT,kotlin.String,pBqfIDcd5QSSbVcp8OvF +base32-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,targetFile,INPUT,kotlin.String,kj6TiBzhFqjXwCfsMO4X +base32-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +base32-encoder-decoder,targetText,INPUT,kotlin.String,OBBHCZSJIRRWINKRKNJWEVTDOA4E65SG +base64-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,sourceFile,INPUT,kotlin.String,r0PacuiHJB3ItWu3zJZS +base64-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +base64-encoder-decoder,sourceText,INPUT,kotlin.String,pjZodxXAhhKka5IW6hba +base64-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,targetFile,INPUT,kotlin.String,njdnx2EyaEocSmI3U8Ap +base64-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +base64-encoder-decoder,targetText,INPUT,kotlin.String,cGpab2R4WEFoaEtrYTVJVzZoYmE= +certificates-download,allowInsecureConnection,CONFIGURATION,kotlin.Boolean,true +certificates-download,followRedirects,CONFIGURATION,kotlin.Boolean,false +certificates-download,url,INPUT,kotlin.String,syVelfewljV0IlQx965U +cli-command-converter,lineBreakDelimiter,CONFIGURATION,kotlin.String,faicM6AcM0P2HJxW2OLI +cli-command-converter,liveConversion,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,sourceFile,INPUT,kotlin.String,wSWNq2ApnpcIPULUKjFI +cli-command-converter,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +cli-command-converter,sourceText,INPUT,kotlin.String,j20IwAM0Rdku02TIwFE0 +cli-command-converter,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,targetFile,INPUT,kotlin.String,bJkzW1HJO6XUmc30RCNg +cli-command-converter,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +cli-command-converter,targetText,INPUT,kotlin.String,j20IwAM0Rdku02TIwFE0 +code-style-formatting,languageId,CONFIGURATION,kotlin.String,XML +code-style-formatting,liveTransformation,CONFIGURATION,kotlin.Boolean,false +code-style-formatting,result-output-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,result-output-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,result-output-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +code-style-formatting,source-input-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,source-input-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,source-input-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +code-style-formatting,sourceText,INPUT,kotlin.String, +color-picker,decimalPlaces,CONFIGURATION,kotlin.Int,3 +color-picker,selectedColor,INPUT,com.intellij.ui.JBColor,-16777032 +cron-expression,cronExpression-CRON4J,INPUT,kotlin.String,bS9fBf39LCeQDCb4FjyM +cron-expression,cronExpression-QUARTZ,INPUT,kotlin.String,CTZsDR7k0OQZiVabRg4n +cron-expression,cronExpression-SPRING,INPUT,kotlin.String,Utq3ZUkgIQCE6a2pWb4G +cron-expression,cronExpression-SPRING53,INPUT,kotlin.String,Evmtw7yec8Og0heWGFtG +cron-expression,cronExpression-UNIX,INPUT,kotlin.String,GfszMXwUX0ZVnG8xdl4r +cron-expression,selectedCronType,CONFIGURATION,CronExpression-CronType,SPRING +csv-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,sourceFile,INPUT,kotlin.String,f8jEBcDh4ep0iCLO6r1B +csv-text-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +csv-text-escape,sourceText,INPUT,kotlin.String,9zVPh0W2fhjgsd7IXWph +csv-text-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,targetFile,INPUT,kotlin.String,3wJ75ka2g94w9NS0hD7P +csv-text-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +csv-text-escape,targetText,INPUT,kotlin.String,9zVPh0W2fhjgsd7IXWph +date-time-converter,formattedIndividual,CONFIGURATION,kotlin.Boolean,true +date-time-converter,formattedIndividualFormat,CONFIGURATION,kotlin.String,ID2U0yDwppBDMz4e9QTn +date-time-converter,formattedLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer,ru-MD +date-time-converter,formattedStandardFormat,CONFIGURATION,DatetimeConverter-StandardFormat,ISO_8601_DATE +date-time-converter,formattedStandardFormatAddOffset,CONFIGURATION,kotlin.Boolean,false +date-time-converter,formattedStandardFormatAddTimeZone,CONFIGURATION,kotlin.Boolean,true +date-time-converter,timeZoneId,CONFIGURATION,kotlin.String,America/Glace_Bay +escape-sequence-escaper-unescaper,backslashsEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,doubleQuotesEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,lineBreaksEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,lineBreaksEscapeSequence,CONFIGURATION,EscapeSequencesEncoderDecoder-LineBreak,LF +escape-sequence-escaper-unescaper,liveConversion,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,singleQuotesEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +escape-sequence-escaper-unescaper,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +escape-sequence-escaper-unescaper,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,sourceFile,INPUT,kotlin.String,DxhUz8PTRTFIyx3sgstP +escape-sequence-escaper-unescaper,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +escape-sequence-escaper-unescaper,sourceText,INPUT,kotlin.String,H99VI1CoiVpzf8PRN7lJ +escape-sequence-escaper-unescaper,tabsEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +escape-sequence-escaper-unescaper,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +escape-sequence-escaper-unescaper,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,targetFile,INPUT,kotlin.String,R3XmAbuMhCDHUXV7R5yN +escape-sequence-escaper-unescaper,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +escape-sequence-escaper-unescaper,targetText,INPUT,kotlin.String,H99VI1CoiVpzf8PRN7lJ +hashing-transformer,algorithm,CONFIGURATION,kotlin.String,WHIRLPOOL +hashing-transformer,liveConversion,CONFIGURATION,kotlin.Boolean,false +hashing-transformer,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +hashing-transformer,sourceFile,INPUT,kotlin.String,Rugj0GbNFZaYJNiIk7NG +hashing-transformer,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +hashing-transformer,sourceText,INPUT,kotlin.String,FCucrlEzDJHZ4qAanCx4 +hashing-transformer,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +hashing-transformer,targetFile,INPUT,kotlin.String,AoiB357yvF01FOzPp8Fc +hashing-transformer,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +hmac-transformer,algorithm,CONFIGURATION,kotlin.String,HMACSKEIN-512-512 +hmac-transformer,liveConversion,CONFIGURATION,kotlin.Boolean,false +hmac-transformer,secretKey,SENSITIVE,kotlin.String,jSxiPusODqEV23mdrzSy +hmac-transformer,secretKeyEncodingMode,CONFIGURATION,HmacTransformer-SecretKeyEncodingMode,BASE32 +hmac-transformer,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +hmac-transformer,sourceFile,INPUT,kotlin.String,qdJRSztgubN2fN9BcGBn +hmac-transformer,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +hmac-transformer,sourceText,INPUT,kotlin.String,pSEjTAOnBxNu6j20s7eT +hmac-transformer,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +hmac-transformer,targetFile,INPUT,kotlin.String,wJWf1KvrgyzE9QLzgW7P +hmac-transformer,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +html-entities-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +html-entities-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +html-entities-escape,sourceFile,INPUT,kotlin.String,1IyFnYMIaUE2CsMYWc2M +html-entities-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +html-entities-escape,sourceText,INPUT,kotlin.String,UBJzxCv7T9iqBQ35FEu2 +html-entities-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +html-entities-escape,targetFile,INPUT,kotlin.String,M12PmWKkQbojKcy4SGEJ +html-entities-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +html-entities-escape,targetText,INPUT,kotlin.String,UBJzxCv7T9iqBQ35FEu2 +http-server,advancedCommandLineOptions,CONFIGURATION,kotlin.String,6dWom6bJPtQEZjE91RAB +http-server,builtInServerMapping,CONFIGURATION,kotlin.String,tm1D2szH1l5sJ7QN3js6 +http-server,builtInServerMapping-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +http-server,builtInServerMapping-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +http-server,builtInServerMapping-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +http-server,customRootDirectory,CONFIGURATION,kotlin.String,rviJkIoHSFatAGZukJJb +http-server,httpServerOutput-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +http-server,httpServerOutput-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +http-server,httpServerOutput-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +http-server,javaExecutableMode,CONFIGURATION,HttpServer-JavaExecutableMode,PATH +http-server,javaExecutablePath,CONFIGURATION,kotlin.String,d68HgfBsjL8sBZ5HGTr5 +http-server,printAllNetworkTraffic,CONFIGURATION,kotlin.Boolean,true +http-server,serverMode,CONFIGURATION,HttpServer-ServerMode,CUSTOM_DIRECTORY +http-server,serverPort,CONFIGURATION,kotlin.Int,8090 +http-server,verboseLogging,CONFIGURATION,kotlin.Boolean,true +java-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +java-text-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +java-text-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +java-text-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +java-text-escape,sourceFile,INPUT,kotlin.String,tU0hTkuIM3mTDheXkawo +java-text-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +java-text-escape,sourceText,INPUT,kotlin.String,C1iJVwDasaGiyEQHTeCk +java-text-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +java-text-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +java-text-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +java-text-escape,targetFile,INPUT,kotlin.String,392cR8bdSWIiZki4BKX8 +java-text-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +java-text-escape,targetText,INPUT,kotlin.String,C1iJVwDasaGiyEQHTeCk +json-path,contentText,INPUT,kotlin.String,SMJDxDYag4PzXxzalPXn +json-path,formatResult,CONFIGURATION,kotlin.Boolean,false +json-path,liveTransformation,CONFIGURATION,kotlin.Boolean,false +json-path,result-output-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-path,result-output-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-path,result-output-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-path,source-input-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-path,source-input-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-path,source-input-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-path,sourceText,INPUT,kotlin.String,GaHLbOuViOL37LugKkG6 +json-schema-validator,data-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,data-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,data-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-schema-validator,dataText,INPUT,kotlin.String,y2AdfhVENP25zFvjFiYJ +json-schema-validator,liveValidation,CONFIGURATION,kotlin.Boolean,false +json-schema-validator,schema-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,schema-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,schema-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-schema-validator,schemaText,INPUT,kotlin.String,11ex6xb5ItSJ3qppnARn +json-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +json-text-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-text-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-text-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-text-escape,sourceFile,INPUT,kotlin.String,8v8oblcHaQD7DwuwwURh +json-text-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +json-text-escape,sourceText,INPUT,kotlin.String,CuBVr4xnu7TPDoMvVe45 +json-text-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-text-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-text-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-text-escape,targetFile,INPUT,kotlin.String,T7JwGZu06laB53SmMzmu +json-text-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +json-text-escape,targetText,INPUT,kotlin.String,CuBVr4xnu7TPDoMvVe45 +jwt-encoder-decoder,algorithm,CONFIGURATION,JwtEncoderDecoder-SignatureAlgorithm,HMAC512 +jwt-encoder-decoder,encoded-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,encoded-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,encoded-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,encodedText,INPUT,kotlin.String,eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.ANCf_8p1AE4ZQs7QuqGAyyfTEgYrKSjKWkhBk5cIn1_2QVr2jEjmM-1tu7EgnyOf_fAsvdFXva8Sv05iTGzETg +jwt-encoder-decoder,header-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,header-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,header-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,headerText,INPUT,kotlin.String,"{ + ""alg"": ""HS512"", + ""typ"": ""JWT"" +}" +jwt-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,payload-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,payload-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,payload-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,payloadText,INPUT,kotlin.String,"{ + ""sub"": ""1234567890"", + ""name"": ""John Doe"", + ""admin"": true, + ""iat"": 1516239022 +}" +jwt-encoder-decoder,privateKey,SENSITIVE,kotlin.String,OCr3Bbtm1Jcy7nrAblDw +jwt-encoder-decoder,secret,SENSITIVE,kotlin.String,9PbJXETQxZ4Hq5lMCxZG +jwt-encoder-decoder,secretKeyEncodingMode,CONFIGURATION,JwtEncoderDecoder-SecretKeyEncodingMode,BASE32 +jwt-encoder-decoder,signingKeyValidation,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,validationJwksJson,INPUT,kotlin.String,S1cuwNjmPFFyraxt4gKc +jwt-encoder-decoder,validationJwksUrl,INPUT,kotlin.String,0NnP2c5J9WjZbkK27w74 +jwt-encoder-decoder,validationKeySource,CONFIGURATION,kotlin.String,WO23wqyzvN50xMc8EXuU +jwt-encoder-decoder,validationPublicKey,SENSITIVE,kotlin.String,Npe9Xhd99avg1uV1nCMD +jwt-encoder-decoder,validationSecret,SENSITIVE,kotlin.String,NdumcfINsFBzml5sn38j +jwt-encoder-decoder,validationSecretKeyEncodingMode,CONFIGURATION,JwtEncoderDecoder-SecretKeyEncodingMode,BASE32 +jwt-encoder-decoder,validationStrictKeyValidation,CONFIGURATION,kotlin.Boolean,true +lorem-ipsum-generator,generated-text-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +lorem-ipsum-generator,generated-text-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +lorem-ipsum-generator,generated-text-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +lorem-ipsum-generator,generatedTextKind,CONFIGURATION,LoremIpsumGenerator-TextMode,WORDS +lorem-ipsum-generator,maxWordsInBullet,CONFIGURATION,kotlin.Int,31 +lorem-ipsum-generator,maxWordsInParagraph,CONFIGURATION,kotlin.Int,101 +lorem-ipsum-generator,minWordsInBullet,CONFIGURATION,kotlin.Int,11 +lorem-ipsum-generator,minWordsInParagraph,CONFIGURATION,kotlin.Int,21 +lorem-ipsum-generator,numberOfValues,CONFIGURATION,kotlin.Int,10 +lorem-ipsum-generator,startWithLoremIpsum,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,sourceFile,INPUT,kotlin.String,uABajngVv67d8B6XWyeh +mime-base64-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +mime-base64-encoder-decoder,sourceText,INPUT,kotlin.String,W25KziwQFDq9eJNgNcxL +mime-base64-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,targetFile,INPUT,kotlin.String,nPAXHLbqdU88eBVAGpsW +mime-base64-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +mime-base64-encoder-decoder,targetText,INPUT,kotlin.String,VzI1S3ppd1FGRHE5ZUpOZ05jeEw= +nano-id-generator,bulk-generation-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +nano-id-generator,bulk-generation-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +nano-id-generator,bulk-generation-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +notes,content-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +notes,content-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +notes,content-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +notes,test,INPUT,kotlin.String,XB083PeLaiA5vFoBPy1M +password-generator,addDigits,CONFIGURATION,kotlin.Boolean,false +password-generator,addSymbols,CONFIGURATION,kotlin.Boolean,false +password-generator,bulk-generation-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +password-generator,bulk-generation-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +password-generator,bulk-generation-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +password-generator,length,CONFIGURATION,kotlin.Int,31 +password-generator,lettersMode,CONFIGURATION,PasswordGenerator-LettersMode,ASCII_ALPHABET_ONLY_LOWERCASE +password-generator,symbols,CONFIGURATION,kotlin.String,xKMWoX0lLELgvANbNCyz +qr-code-generator,AZTEC-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,AZTEC-height,CONFIGURATION,kotlin.Int,251 +qr-code-generator,AZTEC-layers,CONFIGURATION,kotlin.Int,1 +qr-code-generator,AZTEC-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,AZTEC-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODABAR-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,CODABAR-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,CODABAR-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODABAR-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODE_128-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,CODE_128-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,CODE_128-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODE_128-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODE_39-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,CODE_39-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,CODE_39-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODE_39-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODE_93-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,CODE_93-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,CODE_93-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODE_93-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,DATA_MATRIX-compactMode,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,DATA_MATRIX-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,DATA_MATRIX-forceC40,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,DATA_MATRIX-gs1,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,DATA_MATRIX-height,CONFIGURATION,kotlin.Int,251 +qr-code-generator,DATA_MATRIX-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,DATA_MATRIX-symbolShape,CONFIGURATION,QrCode-SymbolShapeHint,FORCE_SQUARE +qr-code-generator,DATA_MATRIX-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,EAN_13-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,EAN_13-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,EAN_13-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,EAN_13-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,EAN_8-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,EAN_8-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,EAN_8-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,EAN_8-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,ITF-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,ITF-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,ITF-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,ITF-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,PDF_417-compactMode,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,PDF_417-compactionModeType,CONFIGURATION,QrCode-Compaction,TEXT +qr-code-generator,PDF_417-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,PDF_417-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,PDF_417-insertEcis,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,PDF_417-margin,CONFIGURATION,kotlin.Int,6 +qr-code-generator,PDF_417-maxColumns,CONFIGURATION,kotlin.Int,51 +qr-code-generator,PDF_417-minColumns,CONFIGURATION,kotlin.Int,2 +qr-code-generator,PDF_417-minRows,CONFIGURATION,kotlin.Int,2 +qr-code-generator,PDF_417-setDimensions,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,PDF_417-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,QR_CODE-compactMode,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,QR_CODE-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,QR_CODE-gs1,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,QR_CODE-height,CONFIGURATION,kotlin.Int,201 +qr-code-generator,QR_CODE-margin,CONFIGURATION,kotlin.Int,1 +qr-code-generator,QR_CODE-maskPattern,CONFIGURATION,kotlin.Int,0 +qr-code-generator,QR_CODE-version,CONFIGURATION,kotlin.Int,1 +qr-code-generator,QR_CODE-width,CONFIGURATION,kotlin.Int,201 +qr-code-generator,UPC_A-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,UPC_A-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,UPC_A-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,UPC_A-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,UPC_E-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,UPC_E-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,UPC_E-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,UPC_E-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,UPC_EAN_EXTENSION-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,UPC_EAN_EXTENSION-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,UPC_EAN_EXTENSION-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,UPC_EAN_EXTENSION-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,backgroundColor,CONFIGURATION,com.intellij.ui.JBColor,-16777201 +qr-code-generator,content-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,content-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,content-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +qr-code-generator,contentText,INPUT,kotlin.String,DT2jH5zfO6ogjaGtw0VK +qr-code-generator,foregroundColor,CONFIGURATION,com.intellij.ui.JBColor,-16777070 +qr-code-generator,format,CONFIGURATION,BarcodeGenerator-Format,DATA_MATRIX +qr-code-generator,liveGeneration,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,extraction-result-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,extraction-result-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,extraction-result-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,extractionPattern,INPUT,kotlin.String,Z1EdY9GRzpiqNvluBsh1 +regular-expression-matcher,input-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,input-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,input-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,inputText,INPUT,kotlin.String,aJBzo2O8JP0CAdUpjDFV +regular-expression-matcher,regexOption,CONFIGURATION,kotlin.Int,1 +regular-expression-matcher,regexText,INPUT,kotlin.String,G2v9Bsys6F97bd4lCbML +regular-expression-matcher,substitution-result-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,substitution-result-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,substitution-result-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,substitutionPattern,INPUT,kotlin.String,tQuuHum4sQfrlsWsvOic +sql-formatting,dialect,CONFIGURATION,SqlFormatter-Dialect,PlSql +sql-formatting,indentSpaces,CONFIGURATION,kotlin.Int,3 +sql-formatting,linesBetweenQueries,CONFIGURATION,kotlin.Int,2 +sql-formatting,liveConversion,CONFIGURATION,kotlin.Boolean,false +sql-formatting,maxColumnLength,CONFIGURATION,kotlin.Int,31 +sql-formatting,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +sql-formatting,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +sql-formatting,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +sql-formatting,sourceFile,INPUT,kotlin.String,ZTBuJxxQTJmXMbmXwImu +sql-formatting,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +sql-formatting,sourceText,INPUT,kotlin.String,kz2QLwD82agGlVOxynYt +sql-formatting,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +sql-formatting,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +sql-formatting,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +sql-formatting,targetFile,INPUT,kotlin.String,Y8w84BAtbDzInqMuAi3E +sql-formatting,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +sql-formatting,uppercase,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,individualDelimiter,CONFIGURATION,kotlin.String,oTyDR7xJxdkQqzRu3vsU +text-case-transformer,inputTextCase,CONFIGURATION,TextCaseTransformer-TextCase,PASCAL_CASE +text-case-transformer,liveConversion,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,originalParsingMode,CONFIGURATION,TextCaseTransformer-OriginalParsingMode,FIXED_TEXT_CASE +text-case-transformer,outputTextCase,CONFIGURATION,TextCaseTransformer-TextCase,PASCAL_SNAKE_CASE +text-case-transformer,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,sourceFile,INPUT,kotlin.String,0fkzJXO7egPIRER8G2Os +text-case-transformer,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-case-transformer,sourceText,INPUT,kotlin.String,rbaSzRRU2AG2IKxJDOXL +text-case-transformer,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,targetFile,INPUT,kotlin.String,43YfipktZPC0XrAieCyw +text-case-transformer,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-diff,firstText,INPUT,kotlin.String,3o3bjR7l7eM1b8z7jcpU +text-diff,secondText,INPUT,kotlin.String,mLnWaDGV1y6XnbdSP14M +text-filter,filteringContainingModeText,INPUT,kotlin.String,ugnDPg7ITBYWk7Iky6qq +text-filter,filteringMode,CONFIGURATION,TextFilterTransformer-FilteringMode,NOT_CONTAINING +text-filter,filteringNotContainingModeText,INPUT,kotlin.String,LgPA6RnmNxsdpOvWZRYZ +text-filter,filteringRegexModeOptions,CONFIGURATION,kotlin.Int,1 +text-filter,filteringRegexModeText,INPUT,kotlin.String,9YkWNSLJzyUk4OWHWou5 +text-filter,liveConversion,CONFIGURATION,kotlin.Boolean,false +text-filter,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-filter,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-filter,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-filter,sourceFile,INPUT,kotlin.String,ZSy6SqLd4msaY0t3Vj0K +text-filter,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-filter,sourceText,INPUT,kotlin.String,HEK87uVVv9khuU2iks62 +text-filter,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-filter,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-filter,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-filter,targetFile,INPUT,kotlin.String,evQgrgInYYTiKzhJONSS +text-filter,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-filter,tokenSelectionMode,CONFIGURATION,TextFilterTransformer-TokenMode,WORD +text-format-converter,firstLanguage,CONFIGURATION,CodeFormattingConverter-Language,YAML +text-format-converter,liveConversion,CONFIGURATION,kotlin.Boolean,false +text-format-converter,secondLanguage,CONFIGURATION,CodeFormattingConverter-Language,XML +text-format-converter,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-format-converter,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-format-converter,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-format-converter,sourceFile,INPUT,kotlin.String,UduVUUPqqNaghM27TmTK +text-format-converter,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-format-converter,sourceText,INPUT,kotlin.String,Szdv3BZxI4mmqYfBIW0d +text-format-converter,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-format-converter,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-format-converter,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-format-converter,targetFile,INPUT,kotlin.String,HxCPB9XrtbyVVPxG64b2 +text-format-converter,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-format-converter,targetText,INPUT,kotlin.String,Szdv3BZxI4mmqYfBIW0d +text-sorting-transformer,caseInsensitive,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,liveConversion,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,removeBlankWords,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,removeDuplicates,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,reverseOrder,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,sortedIndividualJoinWordsDelimiter,CONFIGURATION,kotlin.String,1rAEPbCmw68ryLbLeAbL +text-sorting-transformer,sortedJoinWordsDelimiter,CONFIGURATION,TextSortingTransformer-WordsDelimiter,SPACE +text-sorting-transformer,sortingOrder,CONFIGURATION,TextSortingTransformer-SortingOrder,WORD_LENGTH +text-sorting-transformer,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,sourceFile,INPUT,kotlin.String,aUlKDPbdUnQx8oPuoo8c +text-sorting-transformer,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-sorting-transformer,sourceText,INPUT,kotlin.String,cXlfDJORxHE5BQSVlExl +text-sorting-transformer,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,targetFile,INPUT,kotlin.String,k6On0eq4m4ipijxUZD4d +text-sorting-transformer,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-sorting-transformer,trimWords,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,unsortedIndividualSplitWordsDelimiter,CONFIGURATION,kotlin.String,qgwspRiumDdu2h4zdZya +text-sorting-transformer,unsortedPredefinedDelimiter,CONFIGURATION,TextSortingTransformer-WordsDelimiter,SPACE +text-statistic,text,INPUT,kotlin.String,pDwFuexdCNVC6FbGiuOu +text-statistic,text-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-statistic,text-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-statistic,text-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +ulid-generator,bulk-generation-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ulid-generator,bulk-generation-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ulid-generator,bulk-generation-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +ulid-generator,generateMonotonicUlid,CONFIGURATION,kotlin.Boolean,true +ulid-generator,individualTime,CONFIGURATION,kotlin.Long,1777651542759 +ulid-generator,ulidFormat,CONFIGURATION,UlidGenerator-UlidFormat,TO_LOWERCASE +ulid-generator,useIndividualTime,CONFIGURATION,kotlin.Boolean,true +unarchiver,archiveTreeSortingMode,CONFIGURATION,Unarchiver-SortingMode,FILENAME_DESC +unarchiver,clearTargetDirectory,CONFIGURATION,kotlin.Boolean,true +unarchiver,createArchiveFilenameSubDirectory,CONFIGURATION,kotlin.Boolean,false +unarchiver,createParentDirectories,CONFIGURATION,kotlin.Boolean,false +unarchiver,lastSelectedOpenedDirectoryPath,CONFIGURATION,kotlin.String,ywyfcCDd0TytZY6BCQH5 +unarchiver,lastSelectedTargetDirectoryPath,CONFIGURATION,kotlin.String,TaKUgHO7R6Q4SCPDgTRV +unarchiver,openFileInEditorOnDoubleClick,CONFIGURATION,kotlin.Boolean,false +unarchiver,openTargetDirectoryAfterExtraction,CONFIGURATION,kotlin.Boolean,false +unarchiver,preserveDirectoryStructure,CONFIGURATION,kotlin.Boolean,false +unarchiver,preserveFileAttributes,CONFIGURATION,kotlin.Boolean,false +unarchiver,showArchiveNodeTotalNumberOfChildren,CONFIGURATION,kotlin.Boolean,false +unarchiver,showArchiveNodeUncompressedSize,CONFIGURATION,kotlin.Boolean,false +units-converter,baseConverter_baseTwoInput,INPUT,kotlin.String,000 +units-converter,baseConverter_showOnlyCommonBases,CONFIGURATION,kotlin.Boolean,false +units-converter,dataSizeConverter_bitDataSizeValue,INPUT,java.math.BigDecimal,1 +units-converter,dataSizeConverter_decimalPlaces,CONFIGURATION,kotlin.Int,6 +units-converter,dataSizeConverter_parsingLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer,fil +units-converter,dataSizeConverter_precision,CONFIGURATION,kotlin.Int,51 +units-converter,dataSizeConverter_roundingMode,CONFIGURATION,MathContextUnitConverter-RoundingMode,HALF_DOWN +units-converter,dataSizeConverter_showLargeDataUnits,CONFIGURATION,kotlin.Boolean,true +units-converter,lastSelectedUnitConverterIndex,CONFIGURATION,kotlin.Int,1 +units-converter,timeConverter_decimalPlaces,CONFIGURATION,kotlin.Int,6 +units-converter,timeConverter_parsingLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer,cv-RU +units-converter,timeConverter_precision,CONFIGURATION,kotlin.Int,51 +units-converter,timeConverter_roundingMode,CONFIGURATION,MathContextUnitConverter-RoundingMode,HALF_DOWN +units-converter,timeConverter_timeNanoseconds,INPUT,java.math.BigDecimal,1 +units-converter,transferRateConverter_bitTransferRateValue,INPUT,java.math.BigDecimal,1 +units-converter,transferRateConverter_decimalPlaces,CONFIGURATION,kotlin.Int,6 +units-converter,transferRateConverter_parsingLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer,gu-IN +units-converter,transferRateConverter_precision,CONFIGURATION,kotlin.Int,51 +units-converter,transferRateConverter_roundingMode,CONFIGURATION,MathContextUnitConverter-RoundingMode,HALF_DOWN +units-converter,transferRateConverter_showLargeDataUnits,CONFIGURATION,kotlin.Boolean,true +units-converter,transferRateConverter_timeDimension,CONFIGURATION,TransferRateConverter-TransferRateTimeDimension,MINUTES +units-converter,transferRateConverter_useCombinedAbbreviationNotation,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,sourceFile,INPUT,kotlin.String,H5f3p7USLbOJj5JbgpZS +url-base64-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +url-base64-encoder-decoder,sourceText,INPUT,kotlin.String,GIbfpTEMSgYmW8q8TZyf +url-base64-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,targetFile,INPUT,kotlin.String,1f2IIDBgmzVtBRQhdquI +url-base64-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +url-base64-encoder-decoder,targetText,INPUT,kotlin.String,R0liZnBURU1TZ1ltVzhxOFRaeWY= +url-encoding-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +url-encoding-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +url-encoding-encoder-decoder,sourceFile,INPUT,kotlin.String,eP6MsGoi23KHhi4G1DuF +url-encoding-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +url-encoding-encoder-decoder,sourceText,INPUT,kotlin.String,yEhhgUsNfKTaGktAV2aV +url-encoding-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +url-encoding-encoder-decoder,targetFile,INPUT,kotlin.String,TZfSQPGs55VVYlUSoQtb +url-encoding-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +url-encoding-encoder-decoder,targetText,INPUT,kotlin.String,yEhhgUsNfKTaGktAV2aV +uuid-generator,UUIDv1IndividualMacAddress,CONFIGURATION,kotlin.String,nU5zzH9V9Q83u5Kf5B0s +uuid-generator,UUIDv1LocalInterface,CONFIGURATION,kotlin.String,DEQrfwLIUvvnEWWrhhNN +uuid-generator,UUIDv1MacAddressGenerationMode,CONFIGURATION,MacAddressBasedUuidGenerator-MacAddressGenerationMode,INDIVIDUAL +uuid-generator,UUIDv3IndividualNamespace,CONFIGURATION,kotlin.String,9BinQfpSjvZBDWojwz5v +uuid-generator,UUIDv3Name,CONFIGURATION,kotlin.String,GlX9scQEZ260QGz46RjT +uuid-generator,UUIDv3NamespaceMode,CONFIGURATION,NamespaceAndNameBasedUuidGenerator-NamespaceMode,INDIVIDUAL +uuid-generator,UUIDv3PredefinedNamespace,CONFIGURATION,NamespaceAndNameBasedUuidGenerator-PredefinedNamespace,URL +uuid-generator,UUIDv5IndividualNamespace,CONFIGURATION,kotlin.String,7YP0gDlT80K0CDeKchP0 +uuid-generator,UUIDv5Name,CONFIGURATION,kotlin.String,DXwcWtMXw92VzrCnRvd3 +uuid-generator,UUIDv5NamespaceMode,CONFIGURATION,NamespaceAndNameBasedUuidGenerator-NamespaceMode,INDIVIDUAL +uuid-generator,UUIDv5PredefinedNamespace,CONFIGURATION,NamespaceAndNameBasedUuidGenerator-PredefinedNamespace,URL +uuid-generator,UUIDv6IndividualMacAddress,CONFIGURATION,kotlin.String,4MRkaTtL3nSa2ck0ragZ +uuid-generator,UUIDv6LocalInterface,CONFIGURATION,kotlin.String,PLzcdpNsEi8UVLVvJGb1 +uuid-generator,UUIDv6MacAddressGenerationMode,CONFIGURATION,MacAddressBasedUuidGenerator-MacAddressGenerationMode,INDIVIDUAL +uuid-generator,bulk-generation-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +uuid-generator,bulk-generation-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +uuid-generator,bulk-generation-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +uuid-generator,version,CONFIGURATION,UuidVersion,V5 +xml-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +xml-text-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +xml-text-escape,sourceFile,INPUT,kotlin.String,REUkl1gB5mkYd9zTxdLK +xml-text-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +xml-text-escape,sourceText,INPUT,kotlin.String,4uaSTdBicK25VbDru2EK +xml-text-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +xml-text-escape,targetFile,INPUT,kotlin.String,Ok4jZXvqsTrRwNuz1Xa5 +xml-text-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +xml-text-escape,targetText,INPUT,kotlin.String,4uaSTdBicK25VbDru2EK diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/instance-settings-persisted-state.xml b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/instance-settings-persisted-state.xml new file mode 100644 index 00000000..c7b5f499 --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/instance-settings-persisted-state.xml @@ -0,0 +1,740 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/removed-configuration-properties.csv b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/removed-configuration-properties.csv new file mode 100644 index 00000000..e66e1200 --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/removed-configuration-properties.csv @@ -0,0 +1 @@ +developerToolId,propertyKey diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/renamed-configuration-properties.csv b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/renamed-configuration-properties.csv new file mode 100644 index 00000000..61fd6930 --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/8.0.0/renamed-configuration-properties.csv @@ -0,0 +1 @@ +developerToolId,oldPropertyKey,newPropertyKey