From 4b6e0d38a4ab8bea815b62b3b1b683fc7dc615e2 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 24 Jun 2024 12:39:53 +0200 Subject: [PATCH] IJPL-155974 don't use ResourceBundle API for lookup (UI Designer don't use our Bundle, but JDK bundle, so, the only way to inject our transformation logic, is to override JDK bundle) GitOrigin-RevId: cd3bda6cf13ce94afd82d3a3b34ff7d4b3f62937 --- .../ex/ConfigurableExtensionPointUtil.java | 25 ++- platform/util/api-dump-unreviewed.txt | 1 - .../util/src/com/intellij/AbstractBundle.kt | 15 +- platform/util/src/com/intellij/BundleBase.kt | 153 +++++++++++------- .../com/intellij/IntelliJResourceBundle.kt | 99 ++++++++++++ .../util/src/com/intellij/UtilBundle.java | 4 +- .../src/org/editorconfig/Utils.kt | 15 +- 7 files changed, 225 insertions(+), 87 deletions(-) create mode 100644 platform/util/src/com/intellij/IntelliJResourceBundle.kt diff --git a/platform/platform-impl/src/com/intellij/openapi/options/ex/ConfigurableExtensionPointUtil.java b/platform/platform-impl/src/com/intellij/openapi/options/ex/ConfigurableExtensionPointUtil.java index e2c0fba3e649..9cfc2d276d18 100644 --- a/platform/platform-impl/src/com/intellij/openapi/options/ex/ConfigurableExtensionPointUtil.java +++ b/platform/platform-impl/src/com/intellij/openapi/options/ex/ConfigurableExtensionPointUtil.java @@ -1,7 +1,7 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.options.ex; -import com.intellij.BundleBase; +import com.intellij.IntelliJResourceBundle; import com.intellij.ide.actions.ConfigurablesPatcher; import com.intellij.ide.ui.search.SearchableOptionsRegistrar; import com.intellij.openapi.application.Application; @@ -226,7 +226,7 @@ public final class ConfigurableExtensionPointUtil { LOG.warn("Use to specify custom configurable group: " + groupId); int weight = getInt(bundle, id + ".settings.weight"); String help = getString(bundle, id + ".settings.help.topic"); - String name = getName(bundle, id + ".settings.display.name"); + String name = getString(bundle, id + ".settings.display.name"); String desc = getString(bundle, id + ".settings.description"); if (name != null && project != null) { if (!project.isDefault() && !name.contains("{")) { @@ -237,7 +237,7 @@ public final class ConfigurableExtensionPointUtil { name = StringUtil.first(MessageFormat.format(name, project.getName()), 30, true); } } - node.myValue = new SortedConfigurableGroup(id, name, desc, help, weight); + node.myValue = new SortedConfigurableGroup(id, Objects.requireNonNullElse(name, ""), desc, help, weight); } } if (configurables != null) { @@ -433,20 +433,17 @@ public final class ConfigurableExtensionPointUtil { return null; } - private static @Nls String getString(ResourceBundle bundle, @NonNls String resource) { - if (bundle == null) return null; - try { - return bundle.getString(resource); - } - catch (MissingResourceException ignored) { + private static @Nls @Nullable String getString(ResourceBundle bundle, @NonNls String resource) { + if (bundle == null) { return null; } - } - private static @Nls String getName(ResourceBundle bundle, @NonNls String resource) { - if (bundle == null) return null; + if (bundle instanceof IntelliJResourceBundle b) { + return b.getMessageOrNull(resource); + } + try { - return BundleBase.messageOrDefault(bundle, resource, null); + return bundle.getString(resource); } catch (MissingResourceException ignored) { return null; diff --git a/platform/util/api-dump-unreviewed.txt b/platform/util/api-dump-unreviewed.txt index 7101ff838a58..87ad5c94f591 100644 --- a/platform/util/api-dump-unreviewed.txt +++ b/platform/util/api-dump-unreviewed.txt @@ -31,7 +31,6 @@ f:com.intellij.BundleBase - sf:partialMessage(java.util.ResourceBundle,java.lang.String,I,java.lang.Object[]):java.lang.String - sf:replaceMnemonicAmpersand(java.lang.String):java.lang.String - sf:setTranslationConsumer(java.util.function.BiConsumer):V -- sf:useDefaultValue(java.util.ResourceBundle,java.lang.String,java.lang.String):java.lang.String f:com.intellij.Patches - sf:IBM_JDK_DISABLE_COLLECTION_BUG:Z - sf:JDK_BUG_ID_21275177:Z diff --git a/platform/util/src/com/intellij/AbstractBundle.kt b/platform/util/src/com/intellij/AbstractBundle.kt index b3ebdf479bb5..160df08e27ed 100644 --- a/platform/util/src/com/intellij/AbstractBundle.kt +++ b/platform/util/src/com/intellij/AbstractBundle.kt @@ -4,7 +4,6 @@ package com.intellij import com.intellij.BundleBase.partialMessage -import com.intellij.BundleBase.useDefaultValue import com.intellij.openapi.diagnostic.logger import com.intellij.util.ArrayUtilRt import com.intellij.util.DefaultBundleService @@ -54,14 +53,14 @@ open class AbstractBundle { fun messageOrDefault(bundle: ResourceBundle?, key: @NonNls String, defaultValue: @Nls String?, vararg params: Any?): @Nls String? { return when { bundle == null -> defaultValue - !bundle.containsKey(key) -> postprocessValue(bundle = bundle, value = useDefaultValue(bundle, key, defaultValue), params = params) - else -> BundleBase.messageOrDefault(bundle = bundle, key = key, defaultValue = defaultValue, params = params) + !bundle.containsKey(key) -> postprocessValue(bundle = bundle, value = defaultValue ?: useDefaultValue(bundle, key), params = params) + else -> com.intellij.messageOrDefault(bundle = bundle, key = key, defaultValue = defaultValue, params = params) } } @JvmStatic fun message(bundle: ResourceBundle, key: @NonNls String, vararg params: Any?): @Nls String { - return BundleBase.messageOrDefault(bundle = bundle, key = key, defaultValue = null, params = params) + return com.intellij.messageOrDefault(bundle = bundle, key = key, defaultValue = null, params = params) } @Suppress("HardCodedStringLiteral") @@ -75,7 +74,7 @@ open class AbstractBundle { inline fun resolveResourceBundleWithFallback( loader: ClassLoader, pathToBundle: String, - firstTry: () -> ResourceBundle + firstTry: () -> ResourceBundle, ): ResourceBundle { try { return firstTry() @@ -96,7 +95,7 @@ open class AbstractBundle { @Contract(pure = true) // open only to preserve compatibility open fun getMessage(key: @NonNls String, vararg params: Any?): @Nls String { - return BundleBase.messageOrDefault(bundle = resourceBundle, key = key, defaultValue = null, params = params) + return com.intellij.messageOrDefault(bundle = resourceBundle, key = key, defaultValue = null, params = params) } /** @@ -202,7 +201,7 @@ private object IntelliJResourceControl : ResourceBundle.Control() { val resourceName = (if (bundleName.contains("://")) null else toResourceName(bundleName, "properties")) ?: return null val stream = loader.getResourceAsStream(resourceName) ?: return null return stream.use { - PropertyResourceBundle(InputStreamReader(it, StandardCharsets.UTF_8)) + IntelliJResourceBundle(InputStreamReader(it, StandardCharsets.UTF_8)) } } -} +} \ No newline at end of file diff --git a/platform/util/src/com/intellij/BundleBase.kt b/platform/util/src/com/intellij/BundleBase.kt index f311af96b2fc..88c94528759e 100644 --- a/platform/util/src/com/intellij/BundleBase.kt +++ b/platform/util/src/com/intellij/BundleBase.kt @@ -1,6 +1,12 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("ReplaceGetOrSet") + package com.intellij +import com.intellij.BundleBase.L10N_MARKER +import com.intellij.BundleBase.SHOW_LOCALIZED_MESSAGES +import com.intellij.BundleBase.appendLocalizationSuffix +import com.intellij.BundleBase.getDefaultMessage import com.intellij.BundleBase.replaceMnemonicAmpersand import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.logger @@ -25,15 +31,21 @@ private val SUFFIXES = arrayOf("", "") private var translationConsumer: BiConsumer? = null private val SHOW_DEFAULT_MESSAGES: Boolean = java.lang.Boolean.getBoolean("idea.l10n.english") -// 0: no, 1: show key, 2: only keys -private val SHOW_KEYS: Int = System.getProperty("idea.l10n.keys").let { + +private val getPolicy: GetPolicy = System.getProperty("idea.l10n.keys").let { when (it) { - "true" -> 1 - "only" -> 2 - else -> 0 + "true" -> GetPolicy.APPEND_KEY + "only" -> GetPolicy.ONLY_KEY + else -> GetPolicy.VALUE } } +private enum class GetPolicy { + VALUE, + APPEND_KEY, + ONLY_KEY, +} + object BundleBase { const val MNEMONIC: Char = 0x1B.toChar() const val MNEMONIC_STRING: @NlsSafe String = MNEMONIC.toString() @@ -79,43 +91,17 @@ object BundleBase { @JvmStatic fun message(bundle: ResourceBundle, key: String, vararg params: Any): @Nls String { - return messageOrDefault(bundle = bundle, key = key, defaultValue = null, params = params) + return com.intellij.messageOrDefault(bundle = bundle, key = key, defaultValue = null, params = params) } + @Suppress("DuplicatedCode") @JvmStatic fun messageOrDefault(bundle: ResourceBundle?, key: String, defaultValue: @Nls String?, vararg params: Any?): @Nls String { if (bundle == null) { return defaultValue!! } - var resourceFound = true - val value = try { - bundle.getString(key) - } - catch (e: MissingResourceException) { - resourceFound = false - useDefaultValue(bundle = bundle, key = key, defaultValue = defaultValue) - } - - if (SHOW_KEYS == 2) { - @Suppress("HardCodedStringLiteral") - return key - } - - val result = postprocessValue(bundle = bundle, value = value, params = params) - translationConsumer?.accept(key, result) - return when { - !resourceFound -> result - SHOW_KEYS == 1 && SHOW_DEFAULT_MESSAGES -> { - appendLocalizationSuffix(result = result, suffixToAppend = " ($key=${getDefaultMessage(bundle, key)})") - } - SHOW_KEYS == 1 -> appendLocalizationSuffix(result = result, suffixToAppend = " ($key)") - SHOW_DEFAULT_MESSAGES -> { - appendLocalizationSuffix(result = result, suffixToAppend = " (${getDefaultMessage(bundle, key)})") - } - SHOW_LOCALIZED_MESSAGES -> appendLocalizationSuffix(result = result, suffixToAppend = L10N_MARKER) - else -> result - } + return com.intellij.messageOrDefault(bundle = bundle, key = key, defaultValue = defaultValue, params = params) } @JvmStatic @@ -144,19 +130,6 @@ object BundleBase { return result + suffixToAppend } - @JvmStatic - @Suppress("HardCodedStringLiteral") - fun useDefaultValue(bundle: ResourceBundle?, key: String, defaultValue: @Nls String?): @Nls String { - if (defaultValue != null) { - return defaultValue - } - if (assertOnMissedKeys) { - val bundleName = if (bundle == null) "" else "(${bundle.baseBundleName})" - LOG.error("'$key' is not found in $bundle$bundleName") - } - return "!$key!" - } - @JvmStatic @Contract(pure = true) fun format(value: String, vararg params: Any): String { @@ -244,20 +217,92 @@ private fun quotePattern(message: String): @NlsSafe String { } @Suppress("HardCodedStringLiteral") -internal fun postprocessValue(bundle: ResourceBundle, value: @Nls String, vararg params: Any?): @Nls String { - @Suppress("NAME_SHADOWING") var value = value - value = replaceMnemonicAmpersand(value)!! - if (params.isEmpty() || !value.contains('{')) { +internal fun postprocessValue(bundle: ResourceBundle, value: @Nls String, params: Array?): @Nls String { + @Suppress("NAME_SHADOWING") val value = replaceMnemonicAmpersand(value)!! + if (params.isNullOrEmpty() || !value.contains('{')) { return value } val locale = bundle.locale - return try { + try { val format = if (locale == null) MessageFormat(value) else MessageFormat(value, locale) OrdinalFormat.apply(format) - format.format(params) + return format.format(params) } catch (e: IllegalArgumentException) { - "!invalid format: `$value`!" + return "!invalid format: `$value`!" + } +} + +@Suppress("DuplicatedCode") +@Internal +fun messageOrDefault(bundle: ResourceBundle, key: String, defaultValue: @Nls String?, params: Array?): @Nls String { + if (bundle !is IntelliJResourceBundle || bundle.parent != null) { + @Suppress("HardCodedStringLiteral") + return messageOrDefaultForJdkBundle(bundle = bundle, key = key, defaultValue = defaultValue, params = params) + } + + return bundle.getMessage(key = key, defaultValue = defaultValue, params = params) +} + +@Suppress("DuplicatedCode") +private fun messageOrDefaultForJdkBundle( + bundle: ResourceBundle, + key: String, + defaultValue: @Nls String?, + params: Array?, +): String { + var resourceFound = true + val value = try { + bundle.getString(key) + } + catch (e: MissingResourceException) { + resourceFound = false + defaultValue ?: useDefaultValue(bundle = bundle, key = key) + } + + val result = postprocessValue(bundle = bundle, value = value, params = params) + translationConsumer?.accept(key, result) + return when { + !resourceFound -> result + getPolicy == GetPolicy.APPEND_KEY -> { + appendLocalizationSuffix( + result = result, + suffixToAppend = if (SHOW_DEFAULT_MESSAGES) " ($key=${getDefaultMessage(bundle, key)})" else " ($key)", + ) + } + SHOW_DEFAULT_MESSAGES -> appendLocalizationSuffix(result = result, suffixToAppend = " (${getDefaultMessage(bundle, key)})") + SHOW_LOCALIZED_MESSAGES -> appendLocalizationSuffix(result = result, suffixToAppend = L10N_MARKER) + else -> result + } +} + +internal fun useDefaultValue(bundle: ResourceBundle, @NlsSafe key: String): @Nls String { + if (assertOnMissedKeys) { + LOG.error("'$key' is not found (baseBundleName=${bundle.baseBundleName}, bundle=$bundle)") + } + return "!$key!" +} + +internal fun postProcessResolvedValue( + @NlsSafe value: String, + key: String, + resourceFound: Boolean, + bundle: ResourceBundle, +): String { + translationConsumer?.accept(key, value) + return when { + !resourceFound -> value + getPolicy == GetPolicy.ONLY_KEY -> "$key${L10N_MARKER}$value" + getPolicy == GetPolicy.APPEND_KEY -> { + appendLocalizationSuffix( + result = value, + suffixToAppend = if (SHOW_DEFAULT_MESSAGES) " ($key=${getDefaultMessage(bundle, key)})" else " ($key)", + ) + } + SHOW_DEFAULT_MESSAGES -> appendLocalizationSuffix(result = value, + suffixToAppend = " (${getDefaultMessage(bundle, key)})") + SHOW_LOCALIZED_MESSAGES -> appendLocalizationSuffix(result = value, suffixToAppend = L10N_MARKER) + else -> value } } diff --git a/platform/util/src/com/intellij/IntelliJResourceBundle.kt b/platform/util/src/com/intellij/IntelliJResourceBundle.kt new file mode 100644 index 000000000000..cd65e373203d --- /dev/null +++ b/platform/util/src/com/intellij/IntelliJResourceBundle.kt @@ -0,0 +1,99 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("ReplaceGetOrSet") + +package com.intellij + +import com.intellij.openapi.util.NlsSafe +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Nls +import java.io.Reader +import java.util.* + +@ApiStatus.Internal +class IntelliJResourceBundle internal constructor(reader: Reader) : ResourceBundle() { + private val lookup: Map + + init { + val properties = Properties() + properties.load(reader) + // don't use immutable map - HashMap performance is better (with String keys) + @Suppress("UNCHECKED_CAST") + lookup = HashMap(properties as Map) + } + + val parent: ResourceBundle? + get() = super.parent + + @NlsSafe + fun getMessageOrNull(key: String): String? { + val value = lookup.get(key) ?: return null + return postProcessResolvedValue(value = value, key = key, resourceFound = true, bundle = this) + } + + @NlsSafe + fun getMessage(key: String, defaultValue: @Nls String?, params: Array?): String { + @Suppress("HardCodedStringLiteral") + var value = lookup.get(key) + val resourceFound = if (value == null) { + value = defaultValue ?: useDefaultValue(bundle = this, key = key) + false + } + else { + true + } + + return postProcessResolvedValue( + value = postprocessValue(bundle = this, value = value, params = params), + key = key, + resourceFound = resourceFound, + bundle = this, + ) + } + + // UI Designer uses ResourceBundle directly, via Java API. + // `getMessage` is not called, so we have to provide our own implementation of ResourceBundle + override fun handleGetObject(key: String): String? = getMessageOrNull(key = key) + + override fun getKeys(): Enumeration { + val parent = super.parent + return if (parent == null) Collections.enumeration(lookup.keys) else ResourceBundleWithParentEnumeration(lookup.keys, parent.keys) + } + + override fun handleKeySet(): Set = lookup.keys +} + +private class ResourceBundleWithParentEnumeration( + private var set: Set, + private var enumeration: Enumeration, +) : Enumeration { + private var iterator: Iterator = set.iterator() + private var next: String? = null + + override fun hasMoreElements(): Boolean { + if (next == null) { + if (iterator.hasNext()) { + next = iterator.next() + } + else { + while (next == null && enumeration.hasMoreElements()) { + next = enumeration.nextElement() + if (set.contains(next)) { + next = null + } + } + } + } + return next != null + } + + override fun nextElement(): String? { + if (hasMoreElements()) { + val result = next + next = null + return result + } + else { + throw NoSuchElementException() + } + } +} \ No newline at end of file diff --git a/platform/util/src/com/intellij/UtilBundle.java b/platform/util/src/com/intellij/UtilBundle.java index 7bd89e54c7d6..0556ccb5d1f7 100644 --- a/platform/util/src/com/intellij/UtilBundle.java +++ b/platform/util/src/com/intellij/UtilBundle.java @@ -1,4 +1,4 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij; import org.jetbrains.annotations.Nls; @@ -14,7 +14,7 @@ public final class UtilBundle { private UtilBundle() { } public static @NotNull @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) { - return BundleBase.messageOrDefault(getUtilBundle(), key, null, params); + return BundleBaseKt.messageOrDefault(getUtilBundle(), key, null, params); } private static @NotNull ResourceBundle getUtilBundle() { diff --git a/plugins/editorconfig/src/org/editorconfig/Utils.kt b/plugins/editorconfig/src/org/editorconfig/Utils.kt index c16a4a20b40e..eb3356a9dc1d 100644 --- a/plugins/editorconfig/src/org/editorconfig/Utils.kt +++ b/plugins/editorconfig/src/org/editorconfig/Utils.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.editorconfig import com.intellij.BundleBase @@ -22,11 +22,7 @@ import com.intellij.psi.search.FileTypeIndex import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.LineSeparator import org.ec4j.core.ResourceProperties -import org.editorconfig.configmanagement.ConfigEncodingCharsetUtil -import org.editorconfig.configmanagement.StandardEditorConfigProperties -import org.editorconfig.configmanagement.indentSizeKey -import org.editorconfig.configmanagement.indentStyleKey -import org.editorconfig.configmanagement.tabWidthKey +import org.editorconfig.configmanagement.* import org.editorconfig.language.messages.EditorConfigBundle import org.editorconfig.plugincomponents.EditorConfigPropertiesService import org.editorconfig.settings.EditorConfigSettings @@ -81,9 +77,12 @@ object Utils { fun invalidConfigMessage(project: Project, configValue: String?, configKey: String, filePath: String?) { val message = if (configValue != null) { - BundleBase.messageOrDefault(EditorConfigBundle.bundle.resourceBundle, "invalid.config.value", + BundleBase.messageOrDefault(EditorConfigBundle.bundle.resourceBundle, + "invalid.config.value", null, - configValue, configKey.ifEmpty { "?" }, filePath) + configValue, + configKey.ifEmpty { "?" }, + filePath) } else { EditorConfigBundle.message("read.failure")