From 32fabbf35ce64d37b99cfdc0f75ebe19d541761d Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Mon, 12 Feb 2024 20:16:48 +0100 Subject: [PATCH] feat: show/allow setting decoded base64 values in Secrets, ConfigMaps (#663) Signed-off-by: Andre Dietisheim --- .github/workflows/IJ.yml | 2 +- gradle.properties | 2 +- .../kubernetes/balloon/StringInputBalloon.kt | 193 ++++++++++++++++++ .../inlay/Base64PresentationsFactory.kt | 152 ++++++++++++++ .../editor/inlay/Base64ValueAdapter.kt | 66 ++++++ .../inlay/Base64ValueInlayHintsProvider.kt | 70 +++++++ .../editor/util/ResourceEditorUtils.kt | 185 ++++++++++++++++- src/main/resources/META-INF/plugin.xml | 3 + 8 files changed, 666 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/balloon/StringInputBalloon.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsFactory.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapter.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueInlayHintsProvider.kt diff --git a/.github/workflows/IJ.yml b/.github/workflows/IJ.yml index 13d59c503..5f6f3f004 100644 --- a/.github/workflows/IJ.yml +++ b/.github/workflows/IJ.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - IJ: [IC-2021.1, IC-2021.2, IC-2021.3, IC-2022.1, IC-2022.2, IC-2022.3, IC-2023.1, IC-2023.2, IC-2023.3] + IJ: [IC-2021.3, IC-2022.1, IC-2022.2, IC-2022.3, IC-2023.1, IC-2023.2, IC-2023.3] steps: - uses: actions/checkout@v2 diff --git a/gradle.properties b/gradle.properties index 5b7c2dc17..7a2db44db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -ideaVersion=IC-2022.1 +ideaVersion=IC-2021.3 # build number ranges # https://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html sinceIdeaBuild=221 diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/balloon/StringInputBalloon.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/balloon/StringInputBalloon.kt new file mode 100644 index 000000000..8ad3a681b --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/balloon/StringInputBalloon.kt @@ -0,0 +1,193 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.balloon + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.ui.ComponentValidator +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.ExpirableRunnable +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.wm.IdeFocusManager +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.JBTextField +import com.intellij.util.ui.JBUI +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import java.awt.event.KeyListener +import java.awt.event.MouseEvent +import java.util.function.Supplier +import javax.swing.JPanel +import javax.swing.JTextArea +import javax.swing.text.JTextComponent +import kotlin.math.max + +class StringInputBalloon( + private val value: String, + private val onValidValue: (String) -> Unit, + private val editor: Editor +) { + + companion object { + private const val MAX_WIDTH = 220.0 + private const val MAX_CHARACTERS = 64 + } + + private var isValid = false + + fun show(event: MouseEvent) { + val (field, balloon) = create() + balloon.show(RelativePoint(event), Balloon.Position.above) + val focusManager = IdeFocusManager.getInstance(editor.project) + focusManager.doWhenFocusSettlesDown(onFocused(focusManager, field)) + } + + private fun create(): Pair { + val panel = JPanel(BorderLayout()) + val textComponent = if (value.contains('\n')) { + createTextArea(panel) + } else { + createTextField(panel) + } + val balloon = createBalloon(panel) + val disposable = Disposer.newDisposable() + Disposer.register(balloon, disposable) + ComponentValidator(disposable) + .withValidator(ValueValidator(textComponent)) + .installOn(textComponent) + .andRegisterOnDocumentListener(textComponent) + .revalidate() + val keyListener = onKeyPressed(textComponent, balloon) + textComponent.addKeyListener(keyListener) + balloon.addListener(onClosed(textComponent, keyListener)) + return Pair(textComponent, balloon) + } + + private fun createTextField(panel: JPanel): JBTextField { + val label = JBLabel("Value:") + label.border = JBUI.Borders.empty(0, 3, 0, 1) + panel.add(label, BorderLayout.WEST) + val field = JBTextField(value) + field.preferredSize = Dimension( + max(MAX_WIDTH, field.preferredSize.width.toDouble()).toInt(), + field.preferredSize.height + ) + panel.add(field, BorderLayout.CENTER) + return field + } + + private fun createTextArea(panel: JPanel): JTextArea { + val label = JBLabel("Value:") + label.border = JBUI.Borders.empty(0, 3, 4, 0) + panel.add(label, BorderLayout.NORTH) + val textArea = JBTextArea( + value, + value.length.floorDiv(MAX_CHARACTERS - 1) + 2, // textarea has text lines + 1 + MAX_CHARACTERS - 1 + ) + val scrolled = ScrollPaneFactory.createScrollPane(textArea, true) + panel.add(scrolled, BorderLayout.CENTER) + return textArea + } + + private fun createBalloon(panel: JPanel): Balloon { + return JBPopupFactory.getInstance() + .createBalloonBuilder(panel) + .setCloseButtonEnabled(true) + .setBlockClicksThroughBalloon(true) + .setAnimationCycle(0) + .setHideOnKeyOutside(true) + .setHideOnClickOutside(true) + .setFillColor(panel.background) + .setHideOnAction(false) // allow user to Ctrl+A & Ctrl+C + .createBalloon() + } + + private fun onClosed(field: JTextComponent, keyListener: KeyListener): JBPopupListener { + return object : JBPopupListener { + override fun beforeShown(event: LightweightWindowEvent) { + // do nothing + } + + override fun onClosed(event: LightweightWindowEvent) { + field.removeKeyListener(keyListener) + } + } + } + + private fun onKeyPressed(textComponent: JTextComponent, balloon: Balloon): KeyListener { + return object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + when (e.keyCode) { + KeyEvent.VK_ESCAPE -> + balloon.hide() + + KeyEvent.VK_ENTER -> + if (isValid) { + balloon.hide() + onValidValue.invoke(textComponent.text) + } + } + } + } + } + + private fun onFocused(focusManager: IdeFocusManager, field: JTextComponent): ExpirableRunnable { + return object : ExpirableRunnable { + + override fun run() { + focusManager.requestFocus(field, true) + field.selectAll() + } + + override fun isExpired(): Boolean { + return false + } + } + } + + private inner class ValueValidator(private val textComponent: JTextComponent) : Supplier { + + override fun get(): ValidationInfo? { + if (!textComponent.isEnabled + || !textComponent.isVisible + ) { + return null + } + return validate(textComponent.text) + } + + private fun validate(newValue: String): ValidationInfo? { + val validation = when { + StringUtil.isEmptyOrSpaces(newValue) -> + ValidationInfo("Provide a value", textComponent).asWarning() + + value == newValue -> + ValidationInfo("Provide new value", textComponent).asWarning() + + else -> + null + } + this@StringInputBalloon.isValid = (validation == null) + return validation + } + } + +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsFactory.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsFactory.kt new file mode 100644 index 000000000..651ef2de2 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsFactory.kt @@ -0,0 +1,152 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +@file:Suppress("UnstableApiUsage") +package com.redhat.devtools.intellij.kubernetes.editor.inlay + +import com.intellij.codeInsight.hints.InlayHintsSink +import com.intellij.codeInsight.hints.presentation.InlayPresentation +import com.intellij.codeInsight.hints.presentation.PresentationFactory +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.impl.EditorImpl +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo +import com.redhat.devtools.intellij.kubernetes.balloon.StringInputBalloon +import com.redhat.devtools.intellij.kubernetes.editor.util.getBinaryData +import com.redhat.devtools.intellij.kubernetes.editor.util.getData +import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource +import com.redhat.devtools.intellij.kubernetes.model.util.trimWithEllipsis +import org.jetbrains.concurrency.runAsync +import java.awt.event.MouseEvent + +class Base64PresentationsFactory { + + companion object { + private const val SECRET_RESOURCE_KIND = "Secret" + private const val CONFIGMAP_RESOURCE_KIND = "ConfigMap" + } + + fun create(content: PsiElement, info: KubernetesResourceInfo, editor: Editor, sink: InlayHintsSink): InlayPresentationsFactory? { + return when { + isKubernetesResource(SECRET_RESOURCE_KIND, info) -> + StringPresentationsFactory(content, editor, sink) + + isKubernetesResource(CONFIGMAP_RESOURCE_KIND, info) -> + BinaryPresentationsFactory(content, editor, sink) + + else -> null + } + + } + + abstract class InlayPresentationsFactory( + private val element: PsiElement, + private val editor: Editor, + private val sink: InlayHintsSink + ) { + + companion object { + const val INLAY_HINT_MAX_WIDTH = 50 + } + + fun create(): Collection? { + return getChildren(element)?.children?.mapNotNull { child -> + create(Base64ValueAdapter(child), editor, sink) + } + } + + protected abstract fun getChildren(element: PsiElement): PsiElement? + + protected abstract fun create(adapter: Base64ValueAdapter, editor: Editor, sink: InlayHintsSink): InlayPresentation? + + } + + class StringPresentationsFactory(element: PsiElement, editor: Editor, sink: InlayHintsSink) : + InlayPresentationsFactory(element, editor, sink) { + + override fun getChildren(element: PsiElement): PsiElement? { + return getData(element) + } + + override fun create(adapter: Base64ValueAdapter, editor: Editor, sink: InlayHintsSink): InlayPresentation? { + val decoded = adapter.getDecoded() ?: return null + val offset = adapter.getStartOffset() ?: return null + val onClick = StringInputBalloon( + decoded, + onValidValue(adapter::set, editor.project), + editor + )::show + val presentation = create(decoded, onClick, editor) ?: return null + sink.addInlineElement(offset, false, presentation, false) + return presentation + } + + private fun create(text: String, onClick: (event: MouseEvent) -> Unit, editor: Editor): InlayPresentation? { + val factory = PresentationFactory(editor as EditorImpl) + val trimmed = trimWithEllipsis(text, INLAY_HINT_MAX_WIDTH) ?: return null + val textPresentation = factory.smallText(trimmed) + val hoverPresentation = factory.referenceOnHover(textPresentation) { event, _ -> + onClick.invoke(event) + } + val tooltipPresentation = factory.withTooltip("Click to change value", hoverPresentation) + val roundPresentation = factory.roundWithBackground(tooltipPresentation) + return roundPresentation + } + + fun onValidValue( + setter: (value: String, project: Project?) -> Unit, + project: Project? + ): (value: String) -> Unit { + return { value -> + runAsync { + WriteCommandAction.runWriteCommandAction(project) { + setter.invoke(value, project) + } + } + } + } + + } + + class BinaryPresentationsFactory(element: PsiElement, editor: Editor, sink: InlayHintsSink) : + InlayPresentationsFactory(element, editor, sink) { + + override fun getChildren(element: PsiElement): PsiElement? { + return getBinaryData(element) + } + + override fun create(adapter: Base64ValueAdapter, editor: Editor, sink: InlayHintsSink): InlayPresentation? { + val decoded = adapter.getDecodedBytes() ?: return null + val offset = adapter.getStartOffset() ?: return null + val presentation = create(decoded, editor) ?: return null + sink.addInlineElement(offset, false, presentation, false) + return presentation + } + + private fun create(bytes: ByteArray, editor: Editor): InlayPresentation? { + val factory = PresentationFactory(editor as EditorImpl) + val hex = toHexString(bytes) ?: return null + val trimmed = trimWithEllipsis(hex, INLAY_HINT_MAX_WIDTH) ?: return null + return factory.roundWithBackground(factory.smallText(trimmed)) + } + + private fun toHexString(bytes: ByteArray): String? { + return try { + bytes.joinToString(separator = " ") { byte -> + Integer.toHexString(byte.toInt()) + } + } catch (e: Exception) { + null + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapter.kt new file mode 100644 index 000000000..fe90a1452 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapter.kt @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.editor.inlay + +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64 +import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64ToBytes +import com.redhat.devtools.intellij.kubernetes.editor.util.encodeBase64 +import com.redhat.devtools.intellij.kubernetes.editor.util.getValue +import com.redhat.devtools.intellij.kubernetes.editor.util.setValue + +class Base64ValueAdapter(private val element: PsiElement) { + + private companion object { + private val CONTENT_REGEX = Regex("[^ |\n]*", setOf(RegexOption.MULTILINE)) + private const val START_MULTILINE = "|\n" + } + + fun set(value: String, project: Project?) { + val toSet = if (isMultiline()) { + START_MULTILINE + encodeBase64(value) + ?.chunked(76) + ?.joinToString("\n") + } else { + encodeBase64(value) + } + ?: return + setValue(toSet, element, project) + } + + fun get(): String? { + return getValue(element) + } + + private fun isMultiline(): Boolean { + val value = get() + return value?.startsWith(START_MULTILINE) ?: false + } + + fun getDecoded(): String? { + val value = get() ?: return null + val content = CONTENT_REGEX + .findAll(value) + .filter { matchResult -> matchResult.value.isNotBlank() } + .map { matchResult -> matchResult.value } + .joinToString(separator = "") + return decodeBase64(content) + } + + fun getDecodedBytes(): ByteArray? { + return decodeBase64ToBytes(get()) + } + + fun getStartOffset(): Int? { + return com.redhat.devtools.intellij.kubernetes.editor.util.getStartOffset(element) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueInlayHintsProvider.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueInlayHintsProvider.kt new file mode 100644 index 000000000..ebd17a11b --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueInlayHintsProvider.kt @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +@file:Suppress("UnstableApiUsage") + +package com.redhat.devtools.intellij.kubernetes.editor.inlay + +import com.intellij.codeInsight.hints.ChangeListener +import com.intellij.codeInsight.hints.FactoryInlayHintsCollector +import com.intellij.codeInsight.hints.ImmediateConfigurable +import com.intellij.codeInsight.hints.InlayHintsCollector +import com.intellij.codeInsight.hints.InlayHintsProvider +import com.intellij.codeInsight.hints.InlayHintsSink +import com.intellij.codeInsight.hints.NoSettings +import com.intellij.codeInsight.hints.SettingsKey +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.ui.dsl.builder.panel +import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo +import com.redhat.devtools.intellij.kubernetes.editor.util.getContent +import javax.swing.JComponent + + +internal class Base64ValueInlayHintsProvider : InlayHintsProvider { + + override val key: SettingsKey = SettingsKey("KubernetesResource.hints") + override val name: String = "Kubernetes" + override val previewText: String = "Preview" + + override fun createSettings(): NoSettings { + return NoSettings() + } + + override fun createConfigurable(settings: NoSettings): ImmediateConfigurable { + return object : ImmediateConfigurable { + override fun createComponent(listener: ChangeListener): JComponent = panel {} + + override val mainCheckboxText: String = "Show hints for:" + + override val cases: List = emptyList() + } + } + + override fun getCollectorFor(file: PsiFile, editor: Editor, settings: NoSettings, sink: InlayHintsSink): InlayHintsCollector? { + val info = KubernetesResourceInfo.extractMeta(file) ?: return null + return Collector(editor, info) + } + + private class Collector(editor: Editor, private val info: KubernetesResourceInfo) : FactoryInlayHintsCollector(editor) { + + override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean { + if (element !is PsiFile + || !element.isValid) { + return true + } + val content = getContent(element) ?: return true + val factory = Base64PresentationsFactory().create(content, info, editor, sink) ?: return true + factory.create() ?: return true + return false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt index 10ca4d707..1feca9036 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt @@ -10,6 +10,7 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.editor.util +import com.intellij.json.psi.JsonElement import com.intellij.json.psi.JsonElementGenerator import com.intellij.json.psi.JsonFile import com.intellij.json.psi.JsonProperty @@ -17,19 +18,25 @@ import com.intellij.json.psi.JsonValue import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.Document import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.Strings import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager +import com.intellij.refactoring.suggested.startOffset import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo import org.jetbrains.yaml.YAMLElementGenerator import org.jetbrains.yaml.YAMLUtil import org.jetbrains.yaml.psi.YAMLFile import org.jetbrains.yaml.psi.YAMLKeyValue +import org.jetbrains.yaml.psi.YAMLPsiElement import org.jetbrains.yaml.psi.YAMLValue +import java.util.Base64 private const val KEY_METADATA = "metadata" +private const val KEY_DATA = "data" +private const val KEY_BINARY_DATA = "binaryData" private const val KEY_RESOURCE_VERSION = "resourceVersion" /** @@ -59,6 +66,11 @@ fun isKubernetesResource(resourceInfo: KubernetesResourceInfo?): Boolean { && resourceInfo?.typeInfo?.kind?.isNotBlank() ?: false } +fun isKubernetesResource(kind: String, resourceInfo: KubernetesResourceInfo?): Boolean { + return resourceInfo?.typeInfo?.apiGroup?.isNotBlank() ?: false + && kind == resourceInfo?.typeInfo?.kind +} + /** * Returns [KubernetesResourceInfo] for the given file and project. Returns `null` if it could not be retrieved. * @@ -134,6 +146,13 @@ private fun createOrUpdateResourceVersion(resourceVersion: String, metadata: YAM } } +fun getContent(element: PsiElement): PsiElement? { + if (element !is PsiFile) { + return null + } + return getContent(element) +} + private fun getContent(file: PsiFile): PsiElement? { return when (file) { is YAMLFile -> { @@ -154,32 +173,188 @@ fun getMetadata(document: Document?, psi: PsiDocumentManager): PsiElement? { } val file = psi.getPsiFile(document) ?: return null val content = getContent(file) ?: return null - return getMetadata(content) ?: return null + return getMetadata(content) } +/** + * Returns the [PsiElement] named "metadata" within the children of the given [PsiElement]. + * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. + * + * @param element the PsiElement whose "metadata" child should be found. + * @return the PsiElement named "metadata" + */ private fun getMetadata(content: PsiElement): PsiElement? { return when (content) { is YAMLValue -> content.children - .filterIsInstance(YAMLKeyValue::class.java) + .filterIsInstance() .find { it.name == KEY_METADATA } is JsonValue -> content.children.toList() - .filterIsInstance(JsonProperty::class.java) + .filterIsInstance() .find { it.name == KEY_METADATA } else -> null } } +/** + * Returns the [PsiElement] named "data" within the children of the given [PsiElement]. + * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. + * + * @param element the PsiElement whose "data" child should be found. + * @return the PsiElement named "data" + */ +fun getData(element: PsiElement): PsiElement? { + return when (element) { + is YAMLPsiElement -> + element.children + .filterIsInstance() + .find { it.name == KEY_DATA } + ?.value + is JsonElement -> + element.children.toList() + .filterIsInstance() + .find { it.name == KEY_DATA } + ?.value + else -> + null + } +} + +/** + * Returns the [PsiElement] named "binaryData" within the children of the given [PsiElement]. + * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. + * + * @param element the PsiElement whose "binaryData" child should be found. + * @return the PsiElement named "binaryData" + */ +fun getBinaryData(element: PsiElement): PsiElement? { + return when (element) { + is YAMLPsiElement -> + element.children + .filterIsInstance() + .find { it.name == KEY_BINARY_DATA } + ?.value + is JsonElement -> + element.children.toList() + .filterIsInstance() + .find { it.name == KEY_BINARY_DATA } + ?.value + else -> + null + } +} + +/** + * Returns a base64 decoded String for the given base64 encoded String. + * Returns `null` if decoding fails. + * + * @param value the string to be decoded + * @return a decoded String for the given base64 encoded String. + */ +fun decodeBase64(value: String?): String? { + if (Strings.isEmptyOrSpaces(value)) { + return value + } + return try { + String(Base64.getDecoder().decode(value)) + } catch (e: IllegalArgumentException) { + null + } +} + +/** + * Returns base64 decoded bytes for the given base64 encoded string. + * Returns `null` if decoding fails. + * + * @param value the string to be decoded + * @return decoded bytes for the given base64 encoded string. + */ +fun decodeBase64ToBytes(value: String?): ByteArray? { + if (Strings.isEmptyOrSpaces(value)) { + return value?.toByteArray() + } + return try { + Base64.getDecoder().decode(value) + } catch (e: IllegalArgumentException) { + null + } +} + +/** + * Returns the base64 encoded string of the given string. + * Returns `null` if encoding fails. + * + * @param value the string to be encoded + * @return the base64 encoded string for the given string. + */ +fun encodeBase64(value: String): String? { + if (Strings.isEmptyOrSpaces(value)) { + return value + } + return try { + val bytes = Base64.getEncoder().encode(value.toByteArray()) + String(bytes) + } catch (e: IllegalArgumentException) { + null + } +} + +/** + * Returns the String value of the given [YAMLKeyValue] or [JsonProperty]. + * + * @param element the psi element to retrieve the startOffset from + * @return the startOffset in the value of the given psi element + */ +fun getValue(element: PsiElement): String? { + return when (element) { + is YAMLKeyValue -> element.value?.text + is JsonProperty -> element.value?.text + else -> null + } +} + +/** + * Returns the startOffset in the [YAMLValue] or [JsonValue] of the given [PsiElement]. + * Returns `null` otherwise. + * + * @param element the psi element to retrieve the startOffset from + * @return the startOffset in the value of the given psi element + */ +fun getStartOffset(element: PsiElement): Int? { + return when (element) { + is YAMLKeyValue -> element.value?.startOffset + is JsonProperty -> element.value?.startOffset + else -> null + } +} + +fun setValue(value: String, element: PsiElement, project: Project?) { + if (project == null) { + return + } + val textElement = when (element) { + is YAMLKeyValue -> { + YAMLElementGenerator.getInstance(project).createYamlKeyValue(element.keyText, value) + } + is JsonProperty -> { + JsonElementGenerator(project).createProperty(element.name, value) + } + else -> null + } ?: return + element.parent.addAfter(textElement, element) + element.delete() +} + private fun getResourceVersion(metadata: YAMLKeyValue): YAMLKeyValue? { return metadata.value?.children - ?.filterIsInstance(YAMLKeyValue::class.java) + ?.filterIsInstance() ?.find { it.name == KEY_RESOURCE_VERSION } } private fun getResourceVersion(metadata: JsonProperty): JsonProperty? { return metadata.value?.children?.toList() - ?.filterIsInstance(JsonProperty::class.java) + ?.filterIsInstance() ?.find { it.name == KEY_RESOURCE_VERSION } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index f04365da9..91bcc976b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -206,6 +206,9 @@ +