diff --git a/platform/configuration-store-impl/.editorconfig b/platform/configuration-store-impl/.editorconfig new file mode 100644 index 000000000000..f9dece3e918d --- /dev/null +++ b/platform/configuration-store-impl/.editorconfig @@ -0,0 +1,3 @@ +[*] +# critical subsystem - named params used a lot +max_line_length = 180 \ No newline at end of file diff --git a/platform/configuration-store-impl/src/SaveSessionProducerBase.kt b/platform/configuration-store-impl/src/SaveSessionProducerBase.kt index 25a430d8add0..4b48bfa946f1 100644 --- a/platform/configuration-store-impl/src/SaveSessionProducerBase.kt +++ b/platform/configuration-store-impl/src/SaveSessionProducerBase.kt @@ -9,13 +9,9 @@ import com.intellij.openapi.util.WriteExternalException import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream import com.intellij.openapi.vfs.LargeFileWriteRequestor import com.intellij.openapi.vfs.SafeWriteRequestor -import com.intellij.platform.settings.PersistenceStateComponentPropertyTag -import com.intellij.platform.settings.SetResult -import com.intellij.platform.settings.SettingsController +import com.intellij.platform.settings.* import com.intellij.serialization.SerializationException -import com.intellij.util.xmlb.BeanBinding -import com.intellij.util.xmlb.RootBinding -import com.intellij.util.xmlb.XmlSerializationException +import com.intellij.util.xmlb.* import org.jdom.Element abstract class SaveSessionProducerBase : SaveSessionProducer, SafeWriteRequestor, LargeFileWriteRequestor { @@ -75,14 +71,18 @@ internal fun serializeState(state: Any, componentName: String, pluginId: PluginI else -> { try { val filter = jdomSerializer.getDefaultSerializationFilter() - val binding = __platformSerializer().getRootBinding(state.javaClass) - if (binding is BeanBinding) { - // top level expects not null (null indicates error, an empty element will be omitted) - return binding.serializeProperties(bean = state, preCreatedElement = null, filter = filter) + val rootBinding = __platformSerializer().getRootBinding(state.javaClass) + if (rootBinding is BeanBinding) { + if (controller == null) { + return rootBinding.serializeProperties(bean = state, preCreatedElement = null, filter = filter) + } + else { + return serializeWithController(rootBinding = rootBinding, bean = state, filter = filter, componentName = componentName, pluginId = pluginId, controller = controller) + } } else { // maybe ArrayBinding - return (binding as RootBinding).serialize(bean = state, filter = filter) as Element + return (rootBinding as RootBinding).serialize(bean = state, filter = filter) as Element } } catch (e: SerializationException) { @@ -94,3 +94,37 @@ internal fun serializeState(state: Any, componentName: String, pluginId: PluginI } } } + +private fun serializeWithController( + rootBinding: BeanBinding, + bean: Any, + filter: SkipDefaultsSerializationFilter, + componentName: String, + pluginId: PluginId, + controller: SettingsController, +): Element? { + val keyTags = java.util.List.of(PersistenceStateComponentPropertyTag(componentName)) + var element: Element? = null + for (binding in rootBinding.bindings!!) { + if (bean is SerializationFilter && !bean.accepts(binding.accessor, bean = bean)) { + continue + } + + if (isPropertySkipped(filter = filter, binding = binding, bean = bean, isFilterPropertyItself = true)) { + continue + } + + val key = SettingDescriptor(key = "$componentName.${binding.propertyName}", pluginId = pluginId, tags = keyTags, serializer = JsonElementSettingSerializerDescriptor) + val result = controller.doSetItem(key = key, value = binding.toJson(bean, filter)) + if (result != SetResult.INAPPLICABLE) { + continue + } + + if (element == null) { + element = Element(rootBinding.tagName) + } + + binding.serialize(bean = bean, parent = element, filter = filter) + } + return element +} \ No newline at end of file diff --git a/platform/configuration-store-impl/src/stateSerialization.kt b/platform/configuration-store-impl/src/stateSerialization.kt index b6026a273e4b..9a72701f111d 100644 --- a/platform/configuration-store-impl/src/stateSerialization.kt +++ b/platform/configuration-store-impl/src/stateSerialization.kt @@ -47,7 +47,7 @@ internal fun deserializeStateWithController( val serializer = __platformSerializer() // KotlinxSerializationBinding here is possible, do not cast to BeanBinding - val rootBinding = serializer.getRootBinding(stateClass) as NotNullDeserializeBinding + val rootBinding = serializer.getRootBinding(stateClass) try { if (mergeInto == null) { if (rootBinding !is BeanBinding || controller == null) { @@ -141,7 +141,7 @@ private fun getXmlSerializationState( val keyTags = java.util.List.of(PersistenceStateComponentPropertyTag(componentName)) for (binding in bindings) { - val key = createSettingDescriptor(key = "${componentName}.${binding.accessor.name}", pluginId = pluginId, tags = keyTags) + val key = createSettingDescriptor(key = "${componentName}.${binding.propertyName}", pluginId = pluginId, tags = keyTags) val value = try { controller.doGetItem(key) } diff --git a/platform/configuration-store-impl/testSrc/xml/XmlSerializerListTest.kt b/platform/configuration-store-impl/testSrc/xml/XmlSerializerListTest.kt index 1078237ada49..39effdd2b091 100644 --- a/platform/configuration-store-impl/testSrc/xml/XmlSerializerListTest.kt +++ b/platform/configuration-store-impl/testSrc/xml/XmlSerializerListTest.kt @@ -1,10 +1,8 @@ -// Copyright 2000-2019 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.configurationStore.xml import com.intellij.openapi.components.BaseState -import com.intellij.util.xmlb.annotations.OptionTag -import com.intellij.util.xmlb.annotations.Tag -import com.intellij.util.xmlb.annotations.XCollection +import com.intellij.util.xmlb.annotations.* import org.junit.Test import java.util.* @@ -33,14 +31,25 @@ class XmlSerializerListTest { val data = Bean() data.values = listOf("foo") - testSerializer(""" - - - """, data) + testSerializer( + expectedXml = """ + + + + """, + expectedJson = """ + { + "values": [ + "foo" + ] + } + """, + bean = data, + ) } @Test @@ -118,22 +127,113 @@ class XmlSerializerListTest { val bean = Bean() - testSerializer("""""", bean) + testSerializer( + expectedXml = """""", + expectedJson = """{}""", + bean = bean, + ) bean.specSources.clear() bean.specSources.addAll(listOf(SpecSource("foo"), SpecSource("bar"))) - testSerializer(""" - - - - - - - - """, bean) + testSerializer( + expectedXml = """ + + + + + + + + + """, + expectedJson = """ + { + "specSources": [ + { + "pathOrUrl": "foo" + }, + { + "pathOrUrl": "bar" + } + ] + } + """, + bean = bean, + ) + } + + @Test + fun elementTypes() { + @Tag("selector") + class Selector { + @Attribute + var name = "" + @Attribute + var path = "" + } + + @Transient + abstract class H2 + + abstract class H : H2() { + private val selectors = ArrayList() + + // test mutable list (in-place mutation must not be performed) + @XCollection(propertyElementName = "selectors") + fun getSelectors() = selectors.toMutableList() + + fun setSelectors(value: List) { + selectors.clear() + selectors.addAll(value) + } + } + + class A : H() + class B : H() + class C : H() + + class Bean { + @XCollection(elementTypes = [A::class, B::class, C::class]) + val handlers = ArrayList() + } + + val random = Random(42) + fun s() = Selector().also { it.name = random.nextInt().toString(32); it.path = random.nextInt().toString(32) } + + val bean = Bean() + bean.handlers.add(A().also { it.setSelectors(listOf(s(), s())) }) + bean.handlers.add(B().also { it.setSelectors(listOf(s(), s())) }) + bean.handlers.add(C().also { it.setSelectors(listOf(s(), s())) }) + testSerializer( + expectedXml = """ + + + + """, + bean = bean, + ) } @Test diff --git a/platform/configuration-store-impl/testSrc/xml/XmlSerializerMapTest.kt b/platform/configuration-store-impl/testSrc/xml/XmlSerializerMapTest.kt index 3f70c97166f4..a060d71228dc 100644 --- a/platform/configuration-store-impl/testSrc/xml/XmlSerializerMapTest.kt +++ b/platform/configuration-store-impl/testSrc/xml/XmlSerializerMapTest.kt @@ -41,10 +41,10 @@ internal class XmlSerializerMapTest { """, expectedJson = """ { - "values": { - "foo": "foo" + "values": { + "foo": "boo" + } } - } """, bean = data, ) @@ -95,7 +95,7 @@ internal class XmlSerializerMapTest { { "option": "xxx", "map": { - "a": "a" + "a": "b" } } """, diff --git a/platform/configuration-store-impl/testSrc/xml/XmlSerializerTest.kt b/platform/configuration-store-impl/testSrc/xml/XmlSerializerTest.kt index a31666016979..3fa8df999ce9 100644 --- a/platform/configuration-store-impl/testSrc/xml/XmlSerializerTest.kt +++ b/platform/configuration-store-impl/testSrc/xml/XmlSerializerTest.kt @@ -45,7 +45,7 @@ internal class XmlSerializerTest { expectedJson = """ { "places_map": { - "foo": "foo" + "foo": "bar" } } """, diff --git a/platform/object-serializer/src/xml/KotlinxSerializationBinding.kt b/platform/object-serializer/src/xml/KotlinxSerializationBinding.kt index df0c51095300..9698ec8daced 100644 --- a/platform/object-serializer/src/xml/KotlinxSerializationBinding.kt +++ b/platform/object-serializer/src/xml/KotlinxSerializationBinding.kt @@ -4,8 +4,8 @@ package com.intellij.serialization.xml import com.intellij.openapi.diagnostic.debug import com.intellij.serialization.LOG import com.intellij.util.xml.dom.XmlElement +import com.intellij.util.xmlb.Binding import com.intellij.util.xmlb.DomAdapter -import com.intellij.util.xmlb.NotNullDeserializeBinding import com.intellij.util.xmlb.RootBinding import com.intellij.util.xmlb.SerializationFilter import kotlinx.serialization.ExperimentalSerializationApi @@ -30,7 +30,7 @@ private val lookup = MethodHandles.lookup() private val kotlinMethodType = MethodType.methodType(KSerializer::class.java) @Internal -class KotlinxSerializationBinding(aClass: Class<*>) : NotNullDeserializeBinding, RootBinding { +class KotlinxSerializationBinding(aClass: Class<*>) : Binding, RootBinding { @JvmField val serializer: KSerializer @@ -45,7 +45,7 @@ class KotlinxSerializationBinding(aClass: Class<*>) : NotNullDeserializeBinding, return json.encodeToJsonElement(serializer, bean) } - override fun fromJson(bean: Any?, element: JsonElement) = json.decodeFromJsonElement(serializer, element) + override fun fromJson(currentValue: Any?, element: JsonElement) = json.decodeFromJsonElement(serializer, element) override fun serialize(bean: Any, parent: Element, filter: SerializationFilter?) { val json = encodeToJson(bean) diff --git a/platform/object-serializer/src/xml/xmlSerializer.kt b/platform/object-serializer/src/xml/xmlSerializer.kt index bde555fd3aa0..9c5afa319b6f 100644 --- a/platform/object-serializer/src/xml/xmlSerializer.kt +++ b/platform/object-serializer/src/xml/xmlSerializer.kt @@ -91,7 +91,7 @@ private class JdomSerializerImpl : JdomSerializer { try { @Suppress("UNCHECKED_CAST") - return (serializer.getRootBinding(clazz, clazz) as NotNullDeserializeBinding).deserialize(null, element, adapter) as T + return serializer.getRootBinding(clazz, clazz).deserialize(context = null, element = element, adapter = adapter) as T } catch (e: SerializationException) { throw e diff --git a/platform/settings-local/src/com/intellij/platform/settings/local/JsonMirrorController.kt b/platform/settings-local/src/com/intellij/platform/settings/local/JsonMirrorController.kt index f53cd42b33a6..838bd639872f 100644 --- a/platform/settings-local/src/com/intellij/platform/settings/local/JsonMirrorController.kt +++ b/platform/settings-local/src/com/intellij/platform/settings/local/JsonMirrorController.kt @@ -1,12 +1,33 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("ReplacePutWithAssignment", "ReplaceGetOrSet") + package com.intellij.platform.settings.local +import com.intellij.configurationStore.SettingsSavingComponent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.PathManager import com.intellij.openapi.components.ComponentManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.openapi.extensions.ExtensionNotApplicableException +import com.intellij.openapi.extensions.PluginId import com.intellij.platform.settings.DelegatedSettingsController import com.intellij.platform.settings.GetResult import com.intellij.platform.settings.SetResult import com.intellij.platform.settings.SettingDescriptor +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import java.nio.file.Files +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.toDuration private class JsonMirrorController : DelegatedSettingsController { init { @@ -15,16 +36,65 @@ private class JsonMirrorController : DelegatedSettingsController { } } + private val service by lazy { + service() + } + override fun getItem(key: SettingDescriptor): GetResult { //println("${key.pluginId.idString}/${key.key}") return GetResult.inapplicable() } override fun setItem(key: SettingDescriptor, value: T?): SetResult { + if (value is JsonElement) { + service.setItem(key, value) + //println(value.toString()) + } return SetResult.INAPPLICABLE } override fun createChild(container: ComponentManager): DelegatedSettingsController { return JsonMirrorController() } +} + +@Service +private class JsonMirrorStorage : SettingsSavingComponent { + @OptIn(ExperimentalSerializationApi::class) + private val jsonFormat = Json { + prettyPrint = true + prettyPrintIndent = " " + } + + // yes - save is ignored first 5 minutes + private var lastSaved: Duration = Duration.ZERO + + private val storage = ConcurrentHashMap>() + + fun setItem(key: SettingDescriptor<*>, value: JsonElement) { + storage.computeIfAbsent(key.pluginId) { ConcurrentHashMap() } + .put(key.key, value) + } + + override suspend fun save() { + val exitInProgress = ApplicationManager.getApplication().isExitInProgress + val now = System.currentTimeMillis().toDuration(DurationUnit.MILLISECONDS) + if (!exitInProgress && (now - lastSaved) < 30.seconds) { + return + } + + val keys = storage.keys.toMutableList() + keys.sort() + val jsonMap = LinkedHashMap() + for (key in keys) { + val value = storage.get(key) ?: continue + jsonMap.put(key.idString, JsonObject(LinkedHashMap(value))) + } + val data = jsonFormat.encodeToString(jsonMap) + withContext(Dispatchers.IO) { + Files.writeString(PathManager.getConfigDir().resolve("json-controller-state.json"), data) + } + + lastSaved = now + } } \ No newline at end of file diff --git a/platform/settings-local/src/com/intellij/platform/settings/local/StateStorageBackedByController.kt b/platform/settings-local/src/com/intellij/platform/settings/local/StateStorageBackedByController.kt index de800be62a9e..89737506e748 100644 --- a/platform/settings-local/src/com/intellij/platform/settings/local/StateStorageBackedByController.kt +++ b/platform/settings-local/src/com/intellij/platform/settings/local/StateStorageBackedByController.kt @@ -64,7 +64,7 @@ internal class StateStorageBackedByController( } else -> { try { - val beanBinding = bindingProducer.getRootBinding(stateClass) as NotNullDeserializeBinding + val beanBinding = bindingProducer.getRootBinding(stateClass) if (beanBinding is KotlinxSerializationBinding) { val data = controller.getItem(createSettingDescriptor(componentName, pluginId)) ?: return null return cborFormat.decodeFromByteArray(beanBinding.serializer, data) as T @@ -111,7 +111,7 @@ internal class StateStorageBackedByController( private fun getXmlSerializationState( mergeInto: T?, - beanBinding: NotNullDeserializeBinding, + beanBinding: Binding, componentName: String, pluginId: PluginId, ): T? { diff --git a/platform/settings/intellij.platform.settings.iml b/platform/settings/intellij.platform.settings.iml index 35b20eb1c149..8d6ea04ea2aa 100644 --- a/platform/settings/intellij.platform.settings.iml +++ b/platform/settings/intellij.platform.settings.iml @@ -16,5 +16,6 @@ + \ No newline at end of file diff --git a/platform/settings/src/com/intellij/platform/settings/SettingSerializerDescriptor.kt b/platform/settings/src/com/intellij/platform/settings/SettingSerializerDescriptor.kt index 6b977fe22f75..da60ee00acd0 100644 --- a/platform/settings/src/com/intellij/platform/settings/SettingSerializerDescriptor.kt +++ b/platform/settings/src/com/intellij/platform/settings/SettingSerializerDescriptor.kt @@ -1,7 +1,9 @@ -// 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.platform.settings import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.serializer import org.jetbrains.annotations.ApiStatus.Internal import org.jetbrains.annotations.ApiStatus.NonExtendable @@ -27,3 +29,8 @@ object RawSettingSerializerDescriptor : SettingSerializerDescriptor, override val serializer: KSerializer get() = throw UnsupportedOperationException() } + +@Internal +object JsonElementSettingSerializerDescriptor : SettingSerializerDescriptor, SettingValueSerializer { + override val serializer: KSerializer = serializer() +} \ No newline at end of file diff --git a/platform/util/src/com/intellij/util/xmlb/AccessorBindingWrapper.java b/platform/util/src/com/intellij/util/xmlb/AccessorBindingWrapper.java index d616bc1675ff..b21bcf0a0378 100644 --- a/platform/util/src/com/intellij/util/xmlb/AccessorBindingWrapper.java +++ b/platform/util/src/com/intellij/util/xmlb/AccessorBindingWrapper.java @@ -73,7 +73,7 @@ final class AccessorBindingWrapper implements MultiNodeBinding, NestedBinding { } @Override - public @Nullable Object deserializeUnsafe(@Nullable Object context, @NotNull T element, @NotNull DomAdapter adapter) { + public @Nullable Object deserialize(@Nullable Object context, @NotNull T element, @NotNull DomAdapter adapter) { if (adapter == JdomAdapter.INSTANCE) { return deserializeUnsafe(context, (Element)element); } @@ -109,7 +109,7 @@ final class AccessorBindingWrapper implements MultiNodeBinding, NestedBinding { } } else { - deserializedValue = binding.deserializeUnsafe(currentValue, element, JdomAdapter.INSTANCE); + deserializedValue = binding.deserialize(currentValue, element, JdomAdapter.INSTANCE); } if (currentValue != deserializedValue) { @@ -144,7 +144,7 @@ final class AccessorBindingWrapper implements MultiNodeBinding, NestedBinding { } } else { - deserializedValue = binding.deserializeUnsafe(currentValue, element, XmlDomAdapter.INSTANCE); + deserializedValue = binding.deserialize(currentValue, element, XmlDomAdapter.INSTANCE); } if (currentValue != deserializedValue) { diff --git a/platform/util/src/com/intellij/util/xmlb/AttributeBinding.java b/platform/util/src/com/intellij/util/xmlb/AttributeBinding.java index 7319edc9607f..1b2d98519623 100644 --- a/platform/util/src/com/intellij/util/xmlb/AttributeBinding.java +++ b/platform/util/src/com/intellij/util/xmlb/AttributeBinding.java @@ -70,7 +70,7 @@ final class AttributeBinding implements PrimitiveValueBinding { } @Override - public @Nullable Object deserializeUnsafe(@Nullable Object context, @NotNull T element, @NotNull DomAdapter adapter) { + public @Nullable Object deserialize(@Nullable Object context, @NotNull T element, @NotNull DomAdapter adapter) { return context; } diff --git a/platform/util/src/com/intellij/util/xmlb/BeanBinding.kt b/platform/util/src/com/intellij/util/xmlb/BeanBinding.kt index 429fcf8e226d..54738f205aa3 100644 --- a/platform/util/src/com/intellij/util/xmlb/BeanBinding.kt +++ b/platform/util/src/com/intellij/util/xmlb/BeanBinding.kt @@ -1,6 +1,6 @@ // 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", "ReplacePutWithAssignment") -@file:ApiStatus.Internal +@file:Internal package com.intellij.util.xmlb @@ -23,7 +23,7 @@ import kotlinx.serialization.json.JsonPrimitive import org.jdom.Element import org.jdom.Namespace import org.jdom.Text -import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.ApiStatus.Internal import java.lang.reflect.AccessibleObject import java.lang.reflect.AnnotatedElement import java.lang.reflect.InvocationTargetException @@ -53,9 +53,9 @@ fun getBeanAccessors(aClass: Class<*>): List = PROPERTY_COLLECT internal const val JSON_CLASS_DISCRIMINATOR_KEY: String = "_class" -open class BeanBinding(@JvmField val beanClass: Class<*>) : NotNullDeserializeBinding, RootBinding { +open class BeanBinding(@JvmField val beanClass: Class<*>) : Binding, RootBinding { @JvmField - internal val tagName: String + val tagName: String @JvmField var bindings: Array? = null @@ -321,7 +321,7 @@ fun deserializeBeanInto( data.computeIfAbsent(binding) { ArrayList() }.add(child) } else { - binding.deserializeUnsafe(result, child, XmlDomAdapter) + binding.deserialize(result, child, XmlDomAdapter) } continue@nextNode } @@ -330,7 +330,7 @@ fun deserializeBeanInto( for (i in start until end) { val binding = bindings[i] if (binding is AccessorBindingWrapper && binding.isFlat) { - binding.deserializeUnsafe(result, element, XmlDomAdapter) + binding.deserialize(result, element, XmlDomAdapter) } } @@ -379,7 +379,7 @@ internal fun deserializeBeanInto( } else { accessorNameTracker?.add(binding.accessor.name) - binding.deserializeUnsafe(result, child, JdomAdapter) + binding.deserialize(result, child, JdomAdapter) } continue@nextNode } @@ -388,7 +388,7 @@ internal fun deserializeBeanInto( for (binding in bindings) { if (binding is AccessorBindingWrapper && binding.isFlat) { - binding.deserializeUnsafe(result, element, JdomAdapter) + binding.deserialize(result, element, JdomAdapter) } } @@ -428,14 +428,14 @@ fun deserializeBeanInto(result: Any, element: Element, binding: NestedBinding, c data.add(child) } else { - binding.deserializeUnsafe(result, child, JdomAdapter) + binding.deserialize(result, child, JdomAdapter) break } } } if (binding is AccessorBindingWrapper && binding.isFlat) { - binding.deserializeUnsafe(result, element, JdomAdapter) + binding.deserialize(result, element, JdomAdapter) } return data @@ -452,14 +452,14 @@ fun deserializeBeanFromControllerInto(result: Any, element: XmlElement, binding: data.add(child) } else { - binding.deserializeUnsafe(result, child, XmlDomAdapter) + binding.deserialize(result, child, XmlDomAdapter) break } } } if (binding is AccessorBindingWrapper && binding.isFlat) { - binding.deserializeUnsafe(result, element, XmlDomAdapter) + binding.deserialize(result, element, XmlDomAdapter) } return data @@ -686,7 +686,8 @@ private fun getTagName(aClass: Class<*>): String { private fun getTagNameFromAnnotation(aClass: Class<*>): String? = aClass.getAnnotation(Tag::class.java)?.value?.takeIf { it.isNotEmpty() } -private fun isPropertySkipped(filter: SerializationFilter?, binding: NestedBinding, bean: Any, isFilterPropertyItself: Boolean): Boolean { +@Internal +fun isPropertySkipped(filter: SerializationFilter?, binding: NestedBinding, bean: Any, isFilterPropertyItself: Boolean): Boolean { val accessor = binding.accessor val property = accessor.getAnnotation(Property::class.java) if (property == null || !property.alwaysWrite) { diff --git a/platform/util/src/com/intellij/util/xmlb/Binding.kt b/platform/util/src/com/intellij/util/xmlb/Binding.kt index c3f1efa74306..1fd56a8bb8ba 100644 --- a/platform/util/src/com/intellij/util/xmlb/Binding.kt +++ b/platform/util/src/com/intellij/util/xmlb/Binding.kt @@ -37,7 +37,7 @@ interface Binding { fun init(originalType: Type, serializer: Serializer) { } - fun deserializeUnsafe(context: Any?, element: T, adapter: DomAdapter): Any? + fun deserialize(context: Any?, element: T, adapter: DomAdapter): Any? fun toJson(bean: Any, filter: SerializationFilter?): JsonElement? } @@ -57,10 +57,4 @@ interface MultiNodeBinding : Binding { val isMulti: Boolean fun deserializeList(currentValue: Any?, elements: List, adapter: DomAdapter): Any? -} - -interface NotNullDeserializeBinding : Binding { - fun deserialize(context: Any?, element: T, adapter: DomAdapter): Any - - override fun deserializeUnsafe(context: Any?, element: T, adapter: DomAdapter): Any = deserialize(context = context, element = element, adapter) } \ No newline at end of file diff --git a/platform/util/src/com/intellij/util/xmlb/CollectionBinding.kt b/platform/util/src/com/intellij/util/xmlb/CollectionBinding.kt index 34fdd69bfc38..273432f5c4ef 100644 --- a/platform/util/src/com/intellij/util/xmlb/CollectionBinding.kt +++ b/platform/util/src/com/intellij/util/xmlb/CollectionBinding.kt @@ -8,6 +8,7 @@ import com.intellij.serialization.ClassUtil import com.intellij.serialization.MutableAccessor import com.intellij.util.ArrayUtilRt import com.intellij.util.SmartList +import com.intellij.util.xmlb.annotations.Transient import com.intellij.util.xmlb.annotations.XCollection import kotlinx.serialization.json.* import org.jdom.Attribute @@ -64,13 +65,13 @@ internal class CollectionBinding( @JvmField internal val elementTypes: Array>, private val serializer: Serializer, private val strategy: CollectionStrategy, -) : MultiNodeBinding, NotNullDeserializeBinding, RootBinding { +) : MultiNodeBinding, RootBinding { private var itemBindings: List? = null override fun init(originalType: Type, serializer: Serializer) { assert(itemBindings == null) - val binding = getItemBinding(itemType) + val binding = getItemBinding(itemType, serializer) val elementTypes = elementTypes if (elementTypes.isEmpty()) { itemBindings = if (binding == null) emptyList() else listOf(binding) @@ -86,7 +87,7 @@ internal class CollectionBinding( } for (aClass in elementTypes) { - val b = getItemBinding(aClass) + val b = getItemBinding(aClass, serializer) if (b != null && !itemBindings.contains(b)) { itemBindings.add(b) } @@ -99,8 +100,9 @@ internal class CollectionBinding( override val isMulti: Boolean get() = true - private fun getItemBinding(aClass: Class<*>): Binding? { - return if (ClassUtil.isPrimitive(aClass)) null else serializer.getRootBinding(aClass, aClass) + private fun getItemBinding(aClass: Class<*>, serializer: Serializer): Binding? { + // element types maybe specified explicitly, in this case do not try to resolve binding for a class that marked as transient (to avoid warning "no accessors") + return if (ClassUtil.isPrimitive(aClass) || aClass.isAnnotationPresent(Transient::class.java)) null else serializer.getRootBinding(aClass, aClass) } override fun toJson(bean: Any, filter: SerializationFilter?): JsonArray { @@ -116,7 +118,7 @@ internal class CollectionBinding( continue } - when (val binding = getItemBinding(value.javaClass)) { + when (val binding = getItemBinding(value.javaClass, serializer)) { null -> content.add(primitiveToJsonElement(value)) is BeanBinding -> content.add(binding.toJson(bean = value, filter = filter, includeClassDiscriminator = itemBindings!!.size > 1)) else -> content.add(binding.toJson(value, filter) ?: JsonNull) @@ -239,7 +241,7 @@ internal class CollectionBinding( return } - val binding = getItemBinding(value.javaClass) + val binding = getItemBinding(value.javaClass, serializer) if (binding == null) { val elementName = elementName if (elementName.isEmpty()) { @@ -272,7 +274,7 @@ internal class CollectionBinding( return XmlSerializerImpl.convert(value, itemType) } else { - return binding.deserializeUnsafe(bean, node, adapter) + return binding.deserialize(bean, node, adapter) } } diff --git a/platform/util/src/com/intellij/util/xmlb/CompactCollectionBinding.java b/platform/util/src/com/intellij/util/xmlb/CompactCollectionBinding.java index b6b3ecf947e2..0974f44c15d0 100644 --- a/platform/util/src/com/intellij/util/xmlb/CompactCollectionBinding.java +++ b/platform/util/src/com/intellij/util/xmlb/CompactCollectionBinding.java @@ -18,7 +18,7 @@ import java.util.List; /** * @see com.intellij.util.xmlb.annotations.CollectionBean */ -final class CompactCollectionBinding implements NotNullDeserializeBinding, NestedBinding { +final class CompactCollectionBinding implements Binding, NestedBinding { private final String name; private final MutableAccessor accessor; diff --git a/platform/util/src/com/intellij/util/xmlb/DomAdapter.kt b/platform/util/src/com/intellij/util/xmlb/DomAdapter.kt index 2c3f36dcbbf5..1e6cbb58b634 100644 --- a/platform/util/src/com/intellij/util/xmlb/DomAdapter.kt +++ b/platform/util/src/com/intellij/util/xmlb/DomAdapter.kt @@ -11,8 +11,6 @@ sealed interface DomAdapter { fun firstElement(element: T): T? - fun hasElementContent(element: T): Boolean - fun getAttributeValue(element: T, name: String): String? fun getName(element: T): String @@ -32,8 +30,6 @@ data object JdomAdapter : DomAdapter { override fun firstElement(element: Element): Element? = element.content.firstOrNull() as Element? - override fun hasElementContent(element: Element): Boolean = element.content.any { it is Element } - override fun getAttributeValue(element: Element, name: String): String? = element.getAttributeValue(name) override fun getChildren(element: Element): List = element.children @@ -49,8 +45,6 @@ data object XmlDomAdapter : DomAdapter { override fun firstElement(element: XmlElement): XmlElement? = element.children.firstOrNull() - override fun hasElementContent(element: XmlElement): Boolean = element.children.isNotEmpty() - override fun getAttributeValue(element: XmlElement, name: String): String? = element.getAttributeValue(name) override fun getChildren(element: XmlElement): List = element.children diff --git a/platform/util/src/com/intellij/util/xmlb/JDOMElementBinding.java b/platform/util/src/com/intellij/util/xmlb/JDOMElementBinding.java index e2eae9938cca..3cfab77d8b08 100644 --- a/platform/util/src/com/intellij/util/xmlb/JDOMElementBinding.java +++ b/platform/util/src/com/intellij/util/xmlb/JDOMElementBinding.java @@ -20,7 +20,7 @@ import java.util.List; import static com.intellij.openapi.util.SafeStAXStreamBuilderKt.buildNsUnawareJdom; -final class JDOMElementBinding implements MultiNodeBinding, NestedBinding, NotNullDeserializeBinding { +final class JDOMElementBinding implements MultiNodeBinding, NestedBinding { private final String tagName; private final MutableAccessor accessor; diff --git a/platform/util/src/com/intellij/util/xmlb/MapBinding.java b/platform/util/src/com/intellij/util/xmlb/MapBinding.java index 694d92cbb8b8..53624bb167c5 100644 --- a/platform/util/src/com/intellij/util/xmlb/MapBinding.java +++ b/platform/util/src/com/intellij/util/xmlb/MapBinding.java @@ -115,7 +115,7 @@ final class MapBinding implements MultiNodeBinding, RootBinding { Map content = new LinkedHashMap<>(); for (Object k : keys) { JsonElement kJ = keyOrValueToJson(k, keyBinding, filter); - JsonElement vJ = keyOrValueToJson(k, valueBinding, filter); + JsonElement vJ = keyOrValueToJson(map.get(k), valueBinding, filter); // todo non-primitive keys content.put(kJ == null ? null : ((JsonPrimitive)kJ).getContent(), vJ); } @@ -245,7 +245,7 @@ final class MapBinding implements MultiNodeBinding, RootBinding { } @Override - public @Nullable Object deserializeUnsafe(@Nullable Object context, @NotNull T element, @NotNull DomAdapter adapter) { + public @Nullable Object deserialize(@Nullable Object context, @NotNull T element, @NotNull DomAdapter adapter) { return null; } @@ -359,7 +359,7 @@ final class MapBinding implements MultiNodeBinding, RootBinding { assert binding != null; for (Element element : entry.getChildren()) { if (binding.isBoundTo(element, JdomAdapter.INSTANCE)) { - return binding.deserializeUnsafe(context, element, JdomAdapter.INSTANCE); + return binding.deserialize(context, element, JdomAdapter.INSTANCE); } } } @@ -390,7 +390,7 @@ final class MapBinding implements MultiNodeBinding, RootBinding { assert binding != null; for (XmlElement element : entry.children) { if (binding.isBoundTo(element, XmlDomAdapter.INSTANCE)) { - return binding.deserializeUnsafe(context, element, XmlDomAdapter.INSTANCE); + return binding.deserialize(context, element, XmlDomAdapter.INSTANCE); } } } diff --git a/platform/util/src/com/intellij/util/xmlb/TagBinding.kt b/platform/util/src/com/intellij/util/xmlb/TagBinding.kt index b1b0b017f71f..d48393352a2f 100644 --- a/platform/util/src/com/intellij/util/xmlb/TagBinding.kt +++ b/platform/util/src/com/intellij/util/xmlb/TagBinding.kt @@ -11,7 +11,6 @@ import org.jdom.Content import org.jdom.Element import org.jdom.Namespace import org.jdom.Text -import java.util.* internal class OptionTagBinding( @JvmField internal val binding: Binding?, @@ -38,10 +37,6 @@ internal class OptionTagBinding( override val isPrimitive: Boolean get() = binding == null || converter != null - override fun deserializeUnsafe(context: Any?, element: T, adapter: DomAdapter): Any { - return deserialize(host = context!!, element = element, adapter = adapter) - } - override fun setValue(bean: Any, value: String?) { if (converter == null) { try { @@ -139,56 +134,68 @@ internal class OptionTagBinding( parent.addContent(targetElement) } - private fun deserialize(host: Any, element: T, adapter: DomAdapter): Any { + override fun deserialize(context: Any?, element: T, adapter: DomAdapter): Any { + context!! if (valueAttribute == null) { if (converter == null && binding != null) { if (binding is BeanBinding) { // yes, we must set `null` as well val value = (if (serializeBeanBindingWithoutWrapperTag) element else adapter.firstElement(element))?.let { - binding.deserialize(context = null, element = it, adapter) - } - accessor.set(host, value) - } - else if (adapter.hasElementContent(element)) { - val oldValue = accessor.read(host) - val newValue = deserializeList(binding = binding, currentValue = oldValue, nodes = adapter.getChildren(element), adapter = adapter) - if (oldValue !== newValue) { - accessor.set(host, newValue) + binding.deserialize(context = null, element = it, adapter = adapter) } + accessor.set(context, value) } else if (binding is CollectionBinding || binding is MapBinding) { - val oldValue = accessor.read(host) - // do nothing if the field is already null - if (oldValue != null) { - val newValue = (binding as MultiNodeBinding).deserializeList(currentValue = oldValue, elements = Collections.emptyList(), adapter = adapter) - if (oldValue !== newValue) { - accessor.set(host, newValue) + val nodes = adapter.getChildren(element) + // in-place mutation only for non-writable accessors (final) - getter can return a mutable list, + // and if we mutate it in-place, the deserialization result may be lost (XmlSerializerListTest.elementTypes test) + if (accessor.isWritable) { + // we must pass the current value in any case - collection binding use it to infer a new collection type + val oldValue = accessor.read(context) + if (nodes.isEmpty() && oldValue == null) { + // do nothing if the field is already null + } + else { + val newValue = (binding as MultiNodeBinding).deserializeList(currentValue = oldValue, elements = nodes, adapter = adapter) + accessor.set(context, newValue) + } + } + else { + val oldValue = accessor.read(context) + if (oldValue == null && nodes.isEmpty()) { + // do nothing if the field is already null + } + else { + val newValue = (binding as MultiNodeBinding).deserializeList(currentValue = oldValue, elements = nodes, adapter = adapter) + if (oldValue !== newValue) { + accessor.set(context, newValue) + } } } } else { - accessor.set(host, null) + throw UnsupportedOperationException("Binding $binding is not expected") } } else { - setValue(bean = host, value = adapter.getTextValue(element = element, defaultText = textIfTagValueEmpty)) + setValue(bean = context, value = adapter.getTextValue(element = element, defaultText = textIfTagValueEmpty)) } } else { val value = adapter.getAttributeValue(element, valueAttribute) if (converter == null) { if (binding == null) { - XmlSerializerImpl.doSet(host, value, accessor, ClassUtil.typeToClass(accessor.genericType)) + XmlSerializerImpl.doSet(context, value, accessor, ClassUtil.typeToClass(accessor.genericType)) } else { - accessor.set(host, binding.deserializeUnsafe(host, element, adapter)) + accessor.set(context, binding.deserialize(context, element, adapter)) } } else { - accessor.set(host, value?.let { converter.fromString(it) }) + accessor.set(context, value?.let { converter.fromString(it) }) } } - return host + return context } override fun isBoundTo(element: T, adapter: DomAdapter): Boolean { @@ -211,8 +218,8 @@ internal fun addContent(targetElement: Element, node: Any) { internal fun deserializeList(binding: Binding, currentValue: Any?, nodes: List, adapter: DomAdapter): Any? { return when { - binding is MultiNodeBinding -> binding.deserializeList(currentValue = currentValue, elements = nodes, adapter) - nodes.size == 1 -> binding.deserializeUnsafe(currentValue, nodes.get(0), adapter) + binding is MultiNodeBinding -> binding.deserializeList(currentValue = currentValue, elements = nodes, adapter = adapter) + nodes.size == 1 -> binding.deserialize(context = currentValue, element = nodes.get(0), adapter = adapter) nodes.isEmpty() -> null else -> throw AssertionError("Duplicate data for $binding will be ignored") } diff --git a/platform/util/src/com/intellij/util/xmlb/TextBinding.java b/platform/util/src/com/intellij/util/xmlb/TextBinding.java index 781742992386..831716326102 100644 --- a/platform/util/src/com/intellij/util/xmlb/TextBinding.java +++ b/platform/util/src/com/intellij/util/xmlb/TextBinding.java @@ -48,7 +48,7 @@ final class TextBinding implements PrimitiveValueBinding { @Nullable @Override - public Object deserializeUnsafe(@Nullable Object context, @NotNull T element, @NotNull DomAdapter adapter) { + public Object deserialize(@Nullable Object context, @NotNull T element, @NotNull DomAdapter adapter) { return context; } diff --git a/platform/util/src/com/intellij/util/xmlb/XmlSerializer.java b/platform/util/src/com/intellij/util/xmlb/XmlSerializer.java index 289c623ef9a5..747d695e6690 100644 --- a/platform/util/src/com/intellij/util/xmlb/XmlSerializer.java +++ b/platform/util/src/com/intellij/util/xmlb/XmlSerializer.java @@ -12,6 +12,7 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.net.URL; +import java.util.Objects; public final class XmlSerializer { private XmlSerializer() { @@ -35,8 +36,8 @@ public final class XmlSerializer { @SuppressWarnings("unchecked") public static @NotNull T deserialize(@NotNull Element element, @NotNull Class aClass) throws SerializationException { try { - NotNullDeserializeBinding binding = (NotNullDeserializeBinding)XmlSerializerImpl.serializer.getRootBinding(aClass, aClass); - return (T)binding.deserialize(null, element, JdomAdapter.INSTANCE); + Binding binding = XmlSerializerImpl.serializer.getRootBinding(aClass, aClass); + return (T)Objects.requireNonNull(binding.deserialize(null, element, JdomAdapter.INSTANCE)); } catch (SerializationException e) { throw e; diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/GenericRepository.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/GenericRepository.java index 80abf9d0068a..d2edafa626e7 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/GenericRepository.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/GenericRepository.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.tasks.generic; import com.intellij.openapi.util.Comparing; @@ -36,29 +36,26 @@ import static com.intellij.tasks.generic.TemplateVariable.FactoryVariable; * @author Evgeny.Zakrevsky */ @Tag("Generic") -public class GenericRepository extends BaseRepositoryImpl { - @NonNls public static final String SERVER_URL = "serverUrl"; - @NonNls public static final String USERNAME = "username"; - @NonNls public static final String PASSWORD = "password"; +public final class GenericRepository extends BaseRepositoryImpl { + public static final @NonNls String SERVER_URL = "serverUrl"; + public static final @NonNls String USERNAME = "username"; + public static final @NonNls String PASSWORD = "password"; private final FactoryVariable myServerTemplateVariable = new FactoryVariable(SERVER_URL) { - @NotNull @Override - public String getValue() { + public @NotNull String getValue() { return GenericRepository.this.getUrl(); } }; private final FactoryVariable myUserNameTemplateVariable = new FactoryVariable(USERNAME) { - @NotNull @Override - public String getValue() { + public @NotNull String getValue() { return GenericRepository.this.getUsername(); } }; private final FactoryVariable myPasswordTemplateVariable = new FactoryVariable(PASSWORD, true) { - @NotNull @Override - public String getValue() { + public @NotNull String getValue() { return GenericRepository.this.getPassword(); } }; @@ -76,7 +73,7 @@ public class GenericRepository extends BaseRepositoryImpl { private ResponseType myResponseType = ResponseType.JSON; - private EnumMap myResponseHandlersMap = new EnumMap<>(ResponseType.class); + private EnumMap responseHandlerMap = new EnumMap<>(ResponseType.class); private List myTemplateVariables = new ArrayList<>(); @@ -113,11 +110,11 @@ public class GenericRepository extends BaseRepositoryImpl { myTemplateVariables = other.getTemplateVariables(); mySubtypeName = other.getSubtypeName(); myDownloadTasksInSeparateRequests = other.getDownloadTasksInSeparateRequests(); - myResponseHandlersMap = new EnumMap<>(ResponseType.class); - for (Map.Entry e : other.myResponseHandlersMap.entrySet()) { + responseHandlerMap = new EnumMap<>(ResponseType.class); + for (Map.Entry e : other.responseHandlerMap.entrySet()) { ResponseHandler handler = e.getValue().clone(); handler.setRepository(this); - myResponseHandlersMap.put(e.getKey(), handler); + responseHandlerMap.put(e.getKey(), handler); } } @@ -131,15 +128,14 @@ public class GenericRepository extends BaseRepositoryImpl { mySingleTaskMethodType = HTTPMethod.GET; myResponseType = ResponseType.JSON; myTemplateVariables = new ArrayList<>(); - myResponseHandlersMap = new EnumMap<>(ResponseType.class); - myResponseHandlersMap.put(ResponseType.XML, getXmlResponseHandlerDefault()); - myResponseHandlersMap.put(ResponseType.JSON, getJsonResponseHandlerDefault()); - myResponseHandlersMap.put(ResponseType.TEXT, getTextResponseHandlerDefault()); + responseHandlerMap = new EnumMap<>(ResponseType.class); + responseHandlerMap.put(ResponseType.XML, getXmlResponseHandlerDefault()); + responseHandlerMap.put(ResponseType.JSON, getJsonResponseHandlerDefault()); + responseHandlerMap.put(ResponseType.TEXT, getTextResponseHandlerDefault()); } - @NotNull @Override - public GenericRepository clone() { + public @NotNull GenericRepository clone() { return new GenericRepository(this); } @@ -173,7 +169,7 @@ public class GenericRepository extends BaseRepositoryImpl { } @Override - public Task[] getIssues(@Nullable final String query, final int max, final long since) throws Exception { + public Task[] getIssues(final @Nullable String query, final int max, final long since) throws Exception { if (StringUtil.isEmpty(myTasksListUrl)) { throw new Exception(TaskBundle.message("task.list.url.configuration.parameter.is.mandatory")); } @@ -233,18 +229,16 @@ public class GenericRepository extends BaseRepositoryImpl { return getHttpMethod(requestUrl, myLoginMethodType); } - @Nullable @Override - public Task findTask(@NotNull final String id) throws Exception { + public @Nullable Task findTask(final @NotNull String id) throws Exception { List variables = concat(getAllTemplateVariables(), new TemplateVariable("id", id)); String requestUrl = substituteTemplateVariables(getSingleTaskUrl(), variables); HttpMethod method = getHttpMethod(requestUrl, mySingleTaskMethodType); return getActiveResponseHandler().parseIssue(executeMethod(method)); } - @Nullable @Override - public CancellableConnection createCancellableConnection() { + public @Nullable CancellableConnection createCancellableConnection() { return new CancellableConnection() { @Override protected void doTest() throws Exception { @@ -349,11 +343,11 @@ public class GenericRepository extends BaseRepositoryImpl { } public ResponseHandler getResponseHandler(ResponseType type) { - return myResponseHandlersMap.get(type); + return responseHandlerMap.get(type); } public ResponseHandler getActiveResponseHandler() { - return myResponseHandlersMap.get(myResponseType); + return responseHandlerMap.get(myResponseType); } @XCollection( @@ -363,22 +357,19 @@ public class GenericRepository extends BaseRepositoryImpl { RegExResponseHandler.class } ) - public List getResponseHandlers() { - if (myResponseHandlersMap.isEmpty()) { - return Collections.emptyList(); - } - return Collections.unmodifiableList(new ArrayList<>(myResponseHandlersMap.values())); + public @NotNull List getResponseHandlers() { + return responseHandlerMap.isEmpty() ? Collections.emptyList() : List.copyOf(responseHandlerMap.values()); } @SuppressWarnings("UnusedDeclaration") - public void setResponseHandlers(List responseHandlers) { - myResponseHandlersMap.clear(); + public void setResponseHandlers(@NotNull List responseHandlers) { + responseHandlerMap.clear(); for (ResponseHandler handler : responseHandlers) { - myResponseHandlersMap.put(handler.getResponseType(), handler); + responseHandlerMap.put(handler.getResponseType(), handler); } // ResponseHandler#repository field is excluded from serialization to prevent - // circular dependency so it has to be done manually during serialization process - for (ResponseHandler handler : myResponseHandlersMap.values()) { + // circular dependency, so it has to be done manually during a serialization process + for (ResponseHandler handler : responseHandlerMap.values()) { handler.setRepository(this); } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/RegExResponseHandler.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/RegExResponseHandler.java index 4133f8b45e00..79c7674770ff 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/RegExResponseHandler.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/RegExResponseHandler.java @@ -1,4 +1,4 @@ -// Copyright 2000-2018 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.tasks.generic; import com.intellij.openapi.diagnostic.Logger; @@ -98,7 +98,7 @@ public final class RegExResponseHandler extends ResponseHandler { for (int i = 0; i < max && matcher.find(); i++) { String id = matcher.group(placeholders.indexOf(ID_PLACEHOLDER) + 1); String summary = matcher.group(placeholders.indexOf(SUMMARY_PLACEHOLDER) + 1); - tasks.add(new GenericTask(id, summary, myRepository)); + tasks.add(new GenericTask(id, summary, repository)); } return tasks.toArray(Task.EMPTY_ARRAY); } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/ResponseHandler.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/ResponseHandler.java index 161ec5413507..67ca37e311f7 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/ResponseHandler.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/ResponseHandler.java @@ -1,3 +1,4 @@ +// 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.tasks.generic; import com.intellij.openapi.project.Project; @@ -10,16 +11,16 @@ import javax.swing.*; /** * ResponseHandler subclasses represent different strategies of extracting tasks from - * task server responses (e.g. using regular expressions, XPath, JSONPath, CSS selector etc.) + * task server responses (e.g., using regular expressions, XPath, JSONPath, CSS selector, etc.) * * @see XPathResponseHandler * @see JsonPathResponseHandler * @see RegExResponseHandler * @author Mikhail Golubev */ +@Transient public abstract class ResponseHandler implements Cloneable { - - protected GenericRepository myRepository; + protected GenericRepository repository; /** * Serialization constructor @@ -29,29 +30,24 @@ public abstract class ResponseHandler implements Cloneable { } public ResponseHandler(@NotNull GenericRepository repository) { - myRepository = repository; + this.repository = repository; } public void setRepository(@NotNull GenericRepository repository) { - myRepository = repository; + this.repository = repository; } - @NotNull - @Transient - public GenericRepository getRepository() { - return myRepository; + public @NotNull GenericRepository getRepository() { + return repository; } - @NotNull - public abstract JComponent getConfigurationComponent(@NotNull Project project); + public abstract @NotNull JComponent getConfigurationComponent(@NotNull Project project); - @NotNull - public abstract ResponseType getResponseType(); + public abstract @NotNull ResponseType getResponseType(); public abstract Task @NotNull [] parseIssues(@NotNull String response, int max) throws Exception; - @Nullable - public abstract Task parseIssue(@NotNull String response) throws Exception; + public abstract @Nullable Task parseIssue(@NotNull String response) throws Exception; public abstract boolean isConfigured(); @@ -59,7 +55,8 @@ public abstract class ResponseHandler implements Cloneable { public ResponseHandler clone() { try { return (ResponseHandler) super.clone(); - } catch (CloneNotSupportedException e) { + } + catch (CloneNotSupportedException e) { throw new AssertionError("ResponseHandler#clone() should be supported"); } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/SelectorBasedResponseHandler.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/SelectorBasedResponseHandler.java index 4d71bb5bcf4b..2c9d7e1eded3 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/SelectorBasedResponseHandler.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/generic/SelectorBasedResponseHandler.java @@ -1,6 +1,4 @@ -/* - * Copyright 2000-2017 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.tasks.generic; import com.intellij.openapi.diagnostic.Logger; @@ -29,25 +27,25 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { private static final Logger LOG = Logger.getInstance(SelectorBasedResponseHandler.class); // Supported selector names - @NonNls protected static final String TASKS = "tasks"; + protected static final @NonNls String TASKS = "tasks"; - @NonNls protected static final String SUMMARY = "summary"; - @NonNls protected static final String DESCRIPTION = "description"; - @NonNls protected static final String ISSUE_URL = "issueUrl"; - @NonNls protected static final String CLOSED = "closed"; - @NonNls protected static final String UPDATED = "updated"; - @NonNls protected static final String CREATED = "created"; + protected static final @NonNls String SUMMARY = "summary"; + protected static final @NonNls String DESCRIPTION = "description"; + protected static final @NonNls String ISSUE_URL = "issueUrl"; + protected static final @NonNls String CLOSED = "closed"; + protected static final @NonNls String UPDATED = "updated"; + protected static final @NonNls String CREATED = "created"; - @NonNls protected static final String SINGLE_TASK_ID = "singleTask-id"; - @NonNls protected static final String SINGLE_TASK_SUMMARY = "singleTask-summary"; - @NonNls protected static final String SINGLE_TASK_DESCRIPTION = "singleTask-description"; - @NonNls protected static final String SINGLE_TASK_ISSUE_URL = "singleTask-issueUrl"; - @NonNls protected static final String SINGLE_TASK_CLOSED = "singleTask-closed"; - @NonNls protected static final String SINGLE_TASK_UPDATED = "singleTask-updated"; - @NonNls protected static final String SINGLE_TASK_CREATED = "singleTask-created"; - @NonNls protected static final String ID = "id"; + protected static final @NonNls String SINGLE_TASK_ID = "singleTask-id"; + protected static final @NonNls String SINGLE_TASK_SUMMARY = "singleTask-summary"; + protected static final @NonNls String SINGLE_TASK_DESCRIPTION = "singleTask-description"; + protected static final @NonNls String SINGLE_TASK_ISSUE_URL = "singleTask-issueUrl"; + protected static final @NonNls String SINGLE_TASK_CLOSED = "singleTask-closed"; + protected static final @NonNls String SINGLE_TASK_UPDATED = "singleTask-updated"; + protected static final @NonNls String SINGLE_TASK_CREATED = "singleTask-created"; + protected static final @NonNls String ID = "id"; - protected LinkedHashMap mySelectors = new LinkedHashMap<>(); + protected LinkedHashMap selectors = new LinkedHashMap<>(); /** * Serialization constructor @@ -57,11 +55,11 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { // empty } - protected SelectorBasedResponseHandler(GenericRepository repository) { + protected SelectorBasedResponseHandler(@NotNull GenericRepository repository) { super(repository); // standard selectors setSelectors(List.of( - // matched against list of tasks at whole downloaded from "taskListUrl" + // matched against a list of tasks at whole downloaded from "taskListUrl" new Selector(TASKS), // matched against single tasks extracted from the list downloaded from "taskListUrl" @@ -73,7 +71,7 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { new Selector(CLOSED), new Selector(ISSUE_URL), - // matched against single task downloaded from "singleTaskUrl" + // matched against a single task downloaded from "singleTaskUrl" new Selector(SINGLE_TASK_ID), new Selector(SINGLE_TASK_SUMMARY), new Selector(SINGLE_TASK_DESCRIPTION), @@ -85,35 +83,30 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { } @XCollection(propertyElementName = "selectors") - @NotNull - public List getSelectors() { - return new ArrayList<>(mySelectors.values()); + public @NotNull List getSelectors() { + return new ArrayList<>(selectors.values()); } - public void setSelectors(@NotNull List selectors) { - mySelectors.clear(); + public void setSelectors(@NotNull List selectors) { + this.selectors.clear(); for (Selector selector : selectors) { - mySelectors.put(selector.getName(), selector); + this.selectors.put(selector.getName(), selector); } } /** * Only predefined selectors should be accessed. */ - @NotNull - protected Selector getSelector(@NotNull String name) { - return mySelectors.get(name); + protected @NotNull Selector getSelector(@NotNull String name) { + return selectors.get(name); } - @NotNull - protected String getSelectorPath(@NotNull String name) { - Selector s = getSelector(name); - return s.getPath(); + protected @NotNull String getSelectorPath(@NotNull String name) { + return getSelector(name).getPath(); } - @NotNull @Override - public JComponent getConfigurationComponent(@NotNull Project project) { + public @NotNull JComponent getConfigurationComponent(@NotNull Project project) { FileType fileType = getResponseType().getSelectorFileType(); HighlightedSelectorsTable table = new HighlightedSelectorsTable(fileType, project, getSelectors()); return new JBScrollPane(table); @@ -122,9 +115,9 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { @Override public SelectorBasedResponseHandler clone() { SelectorBasedResponseHandler clone = (SelectorBasedResponseHandler)super.clone(); - clone.mySelectors = new LinkedHashMap<>(mySelectors.size()); - for (Selector selector : mySelectors.values()) { - clone.mySelectors.put(selector.getName(), selector.clone()); + clone.selectors = new LinkedHashMap<>(selectors.size()); + for (Selector selector : selectors.values()) { + clone.selectors.put(selector.getName(), selector.clone()); } return clone; } @@ -134,7 +127,7 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { Selector idSelector = getSelector(ID); if (StringUtil.isEmpty(idSelector.getPath())) return false; Selector summarySelector = getSelector(SUMMARY); - if (StringUtil.isEmpty(summarySelector.getPath()) && !myRepository.getDownloadTasksInSeparateRequests()) return false; + if (StringUtil.isEmpty(summarySelector.getPath()) && !repository.getDownloadTasksInSeparateRequests()) return false; return true; } @@ -143,21 +136,21 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { if (this == o) return true; if (!(o instanceof SelectorBasedResponseHandler handler)) return false; - if (!mySelectors.equals(handler.mySelectors)) return false; + if (!selectors.equals(handler.selectors)) return false; return true; } @Override public int hashCode() { - return mySelectors.hashCode(); + return selectors.hashCode(); } @Override public final Task @NotNull [] parseIssues(@NotNull String response, int max) throws Exception { if (StringUtil.isEmpty(getSelectorPath(TASKS)) || StringUtil.isEmpty(getSelectorPath(ID)) || - (StringUtil.isEmpty(getSelectorPath(SUMMARY)) && !myRepository.getDownloadTasksInSeparateRequests())) { + (StringUtil.isEmpty(getSelectorPath(SUMMARY)) && !repository.getDownloadTasksInSeparateRequests())) { throw new Exception("Selectors 'tasks', 'id' and 'summary' are mandatory"); } List tasks = selectTasksList(response, max); @@ -166,13 +159,13 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { for (Object context : tasks) { String id = selectString(getSelector(ID), context); GenericTask task; - if (myRepository.getDownloadTasksInSeparateRequests()) { - task = new GenericTask(id, "", myRepository); + if (repository.getDownloadTasksInSeparateRequests()) { + task = new GenericTask(id, "", repository); } else { String summary = selectString(getSelector(SUMMARY), context); assert id != null && summary != null; - task = new GenericTask(id, summary, myRepository); + task = new GenericTask(id, summary, repository); String description = selectString(getSelector(DESCRIPTION), context); if (description != null) { task.setDescription(description); @@ -199,8 +192,7 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { return result.toArray(Task.EMPTY_ARRAY); } - @Nullable - private Date selectDate(@NotNull Selector selector, @NotNull Object context) throws Exception { + private @Nullable Date selectDate(@NotNull Selector selector, @NotNull Object context) throws Exception { String s = selectString(selector, context); if (s == null) { return null; @@ -208,8 +200,7 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { return TaskUtil.parseDate(s); } - @Nullable - protected Boolean selectBoolean(@NotNull Selector selector, @NotNull Object context) throws Exception { + protected @Nullable Boolean selectBoolean(@NotNull Selector selector, @NotNull Object context) throws Exception { String s = selectString(selector, context); if (s == null) { return null; @@ -225,23 +216,21 @@ public abstract class SelectorBasedResponseHandler extends ResponseHandler { String.format("Expression '%s' should match boolean value. Got '%s' instead", selector.getName(), s)); } - @NotNull - protected abstract List selectTasksList(@NotNull String response, int max) throws Exception; + protected abstract @NotNull List selectTasksList(@NotNull String response, int max) throws Exception; - @Nullable - protected abstract @Nls String selectString(@NotNull Selector selector, @NotNull Object context) throws Exception; + protected abstract @Nullable @Nls String selectString(@NotNull Selector selector, @NotNull Object context) throws Exception; - @Nullable @Override - public final Task parseIssue(@NotNull String response) throws Exception { + public final @NotNull Task parseIssue(@NotNull String response) throws Exception { if (StringUtil.isEmpty(getSelectorPath(SINGLE_TASK_ID)) || StringUtil.isEmpty(getSelectorPath(SINGLE_TASK_SUMMARY))) { throw new Exception("Selectors 'singleTask-id' and 'singleTask-summary' are mandatory"); } + String id = selectString(getSelector(SINGLE_TASK_ID), response); String summary = selectString(getSelector(SINGLE_TASK_SUMMARY), response); assert id != null && summary != null; - GenericTask task = new GenericTask(id, summary, myRepository); + GenericTask task = new GenericTask(id, summary, repository); String description = selectString(getSelector(SINGLE_TASK_DESCRIPTION), response); if (description != null) { task.setDescription(description); diff --git a/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/AsanaIntegrationTest.java b/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/AsanaIntegrationTest.java index bfa073145c05..925a6a5366a2 100644 --- a/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/AsanaIntegrationTest.java +++ b/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/AsanaIntegrationTest.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2013 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the 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.tasks.integration; import com.intellij.tasks.Task; @@ -89,7 +75,7 @@ public class AsanaIntegrationTest extends GenericSubtypeTestCase { } public void testParsingTaskList() throws Exception { - // Don't forget to extract summary here, even though it doesn't happen actually when myRepository#getIssues is called + // Don't forget to extract summary here, even though it doesn't happen when myRepository#getIssues is called myRepository.setDownloadTasksInSeparateRequests(false); Task[] tasks = myRepository.getActiveResponseHandler().parseIssues(TASK_LIST_RESPONSE, 50); List expected = List.of(new GenericTask("5479650606120", "Task #1", myRepository),