IJPL-136 SC PSC bridge - set

GitOrigin-RevId: 85cb181bfc859b90c9b75e15ea30e83b5a0d46ad
This commit is contained in:
Vladimir Krivosheev
2024-02-26 11:29:51 +01:00
committed by intellij-monorepo-bot
parent 295ce04b73
commit 6771806ea0
29 changed files with 432 additions and 255 deletions

View File

@@ -0,0 +1,3 @@
[*]
# critical subsystem - named params used a lot
max_line_length = 180

View File

@@ -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
}

View File

@@ -47,7 +47,7 @@ internal fun <T : Any> 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 <T : Any> 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)
}

View File

@@ -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("""
<bean>
<option name="values">
<list>
<option value="foo" />
</list>
</option>
</bean>""", data)
testSerializer(
expectedXml = """
<bean>
<option name="values">
<list>
<option value="foo" />
</list>
</option>
</bean>
""",
expectedJson = """
{
"values": [
"foo"
]
}
""",
bean = data,
)
}
@Test
@@ -118,22 +127,113 @@ class XmlSerializerListTest {
val bean = Bean()
testSerializer("""<bean />""", bean)
testSerializer(
expectedXml = """<bean />""",
expectedJson = """{}""",
bean = bean,
)
bean.specSources.clear()
bean.specSources.addAll(listOf(SpecSource("foo"), SpecSource("bar")))
testSerializer("""
<bean>
<specSources>
<SpecSource>
<option name="pathOrUrl" value="foo" />
</SpecSource>
<SpecSource>
<option name="pathOrUrl" value="bar" />
</SpecSource>
</specSources>
</bean>""", bean)
testSerializer(
expectedXml = """
<bean>
<specSources>
<SpecSource>
<option name="pathOrUrl" value="foo" />
</SpecSource>
<SpecSource>
<option name="pathOrUrl" value="bar" />
</SpecSource>
</specSources>
</bean>
""",
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<Selector>()
// test mutable list (in-place mutation must not be performed)
@XCollection(propertyElementName = "selectors")
fun getSelectors() = selectors.toMutableList()
fun setSelectors(value: List<Selector>) {
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<H>()
}
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>
<option name="handlers">
<A>
<selectors>
<selector name="-12rsomb" path="6vt2nn" />
<selector name="-18hgh0v" path="64bg18" />
</selectors>
</A>
<B>
<selectors>
<selector name="17ggf74" path="-7d8h5l" />
<selector name="13et7c3" path="-15d6ukj" />
</selectors>
</B>
<C>
<selectors>
<selector name="-1apt5a2" path="bm234q" />
<selector name="-cbp623" path="1pob5us" />
</selectors>
</C>
</option>
</Bean>
""",
bean = bean,
)
}
@Test

View File

@@ -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"
}
}
""",

View File

@@ -45,7 +45,7 @@ internal class XmlSerializerTest {
expectedJson = """
{
"places_map": {
"foo": "foo"
"foo": "bar"
}
}
""",

View File

@@ -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<Any>
@@ -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)

View File

@@ -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

View File

@@ -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<JsonMirrorStorage>()
}
override fun <T : Any> getItem(key: SettingDescriptor<T>): GetResult<T?> {
//println("${key.pluginId.idString}/${key.key}")
return GetResult.inapplicable()
}
override fun <T : Any> setItem(key: SettingDescriptor<T>, 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<PluginId, ConcurrentHashMap<String, JsonElement>>()
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<String, JsonElement>()
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
}
}

View File

@@ -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 <T : Any> getXmlSerializationState(
mergeInto: T?,
beanBinding: NotNullDeserializeBinding,
beanBinding: Binding,
componentName: String,
pluginId: PluginId,
): T? {

View File

@@ -16,5 +16,6 @@
<orderEntry type="module" module-name="intellij.platform.extensions" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="library" name="kotlinx-serialization-core" level="project" />
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
</component>
</module>

View File

@@ -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<ByteArray>,
override val serializer: KSerializer<ByteArray>
get() = throw UnsupportedOperationException()
}
@Internal
object JsonElementSettingSerializerDescriptor : SettingSerializerDescriptor<JsonElement>, SettingValueSerializer<JsonElement> {
override val serializer: KSerializer<JsonElement> = serializer()
}

View File

@@ -73,7 +73,7 @@ final class AccessorBindingWrapper implements MultiNodeBinding, NestedBinding {
}
@Override
public @Nullable <T> Object deserializeUnsafe(@Nullable Object context, @NotNull T element, @NotNull DomAdapter<T> adapter) {
public @Nullable <T> Object deserialize(@Nullable Object context, @NotNull T element, @NotNull DomAdapter<T> 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) {

View File

@@ -70,7 +70,7 @@ final class AttributeBinding implements PrimitiveValueBinding {
}
@Override
public @Nullable <T> Object deserializeUnsafe(@Nullable Object context, @NotNull T element, @NotNull DomAdapter<T> adapter) {
public @Nullable <T> Object deserialize(@Nullable Object context, @NotNull T element, @NotNull DomAdapter<T> adapter) {
return context;
}

View File

@@ -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<MutableAccessor> = 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<NestedBinding>? = 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) {

View File

@@ -37,7 +37,7 @@ interface Binding {
fun init(originalType: Type, serializer: Serializer) {
}
fun <T : Any> deserializeUnsafe(context: Any?, element: T, adapter: DomAdapter<T>): Any?
fun <T : Any> deserialize(context: Any?, element: T, adapter: DomAdapter<T>): Any?
fun toJson(bean: Any, filter: SerializationFilter?): JsonElement?
}
@@ -57,10 +57,4 @@ interface MultiNodeBinding : Binding {
val isMulti: Boolean
fun <T : Any> deserializeList(currentValue: Any?, elements: List<T>, adapter: DomAdapter<T>): Any?
}
interface NotNullDeserializeBinding : Binding {
fun <T : Any> deserialize(context: Any?, element: T, adapter: DomAdapter<T>): Any
override fun <T : Any> deserializeUnsafe(context: Any?, element: T, adapter: DomAdapter<T>): Any = deserialize(context = context, element = element, adapter)
}

View File

@@ -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<Class<*>>,
private val serializer: Serializer,
private val strategy: CollectionStrategy,
) : MultiNodeBinding, NotNullDeserializeBinding, RootBinding {
) : MultiNodeBinding, RootBinding {
private var itemBindings: List<Binding>? = 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)
}
}

View File

@@ -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;

View File

@@ -11,8 +11,6 @@ sealed interface DomAdapter<T : Any> {
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<Element> {
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> = element.children
@@ -49,8 +45,6 @@ data object XmlDomAdapter : DomAdapter<XmlElement> {
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<XmlElement> = element.children

View File

@@ -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;

View File

@@ -115,7 +115,7 @@ final class MapBinding implements MultiNodeBinding, RootBinding {
Map<String, JsonElement> 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 <T> Object deserializeUnsafe(@Nullable Object context, @NotNull T element, @NotNull DomAdapter<T> adapter) {
public @Nullable <T> Object deserialize(@Nullable Object context, @NotNull T element, @NotNull DomAdapter<T> 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);
}
}
}

View File

@@ -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 <T : Any> deserializeUnsafe(context: Any?, element: T, adapter: DomAdapter<T>): 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 <T : Any> deserialize(host: Any, element: T, adapter: DomAdapter<T>): Any {
override fun <T : Any> deserialize(context: Any?, element: T, adapter: DomAdapter<T>): 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 <T : Any> isBoundTo(element: T, adapter: DomAdapter<T>): Boolean {
@@ -211,8 +218,8 @@ internal fun addContent(targetElement: Element, node: Any) {
internal fun <T : Any> deserializeList(binding: Binding, currentValue: Any?, nodes: List<T>, adapter: DomAdapter<T>): 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")
}

View File

@@ -48,7 +48,7 @@ final class TextBinding implements PrimitiveValueBinding {
@Nullable
@Override
public <T> Object deserializeUnsafe(@Nullable Object context, @NotNull T element, @NotNull DomAdapter<T> adapter) {
public <T> Object deserialize(@Nullable Object context, @NotNull T element, @NotNull DomAdapter<T> adapter) {
return context;
}

View File

@@ -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> T deserialize(@NotNull Element element, @NotNull Class<T> 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;

View File

@@ -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<ResponseType, ResponseHandler> myResponseHandlersMap = new EnumMap<>(ResponseType.class);
private EnumMap<ResponseType, ResponseHandler> responseHandlerMap = new EnumMap<>(ResponseType.class);
private List<TemplateVariable> 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<ResponseType, ResponseHandler> e : other.myResponseHandlersMap.entrySet()) {
responseHandlerMap = new EnumMap<>(ResponseType.class);
for (Map.Entry<ResponseType, ResponseHandler> 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<TemplateVariable> 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<ResponseHandler> getResponseHandlers() {
if (myResponseHandlersMap.isEmpty()) {
return Collections.emptyList();
}
return Collections.unmodifiableList(new ArrayList<>(myResponseHandlersMap.values()));
public @NotNull List<ResponseHandler> getResponseHandlers() {
return responseHandlerMap.isEmpty() ? Collections.emptyList() : List.copyOf(responseHandlerMap.values());
}
@SuppressWarnings("UnusedDeclaration")
public void setResponseHandlers(List<ResponseHandler> responseHandlers) {
myResponseHandlersMap.clear();
public void setResponseHandlers(@NotNull List<ResponseHandler> 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);
}
}

View File

@@ -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);
}

View File

@@ -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");
}
}

View File

@@ -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<String, Selector> mySelectors = new LinkedHashMap<>();
protected LinkedHashMap<String, Selector> 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<Selector> getSelectors() {
return new ArrayList<>(mySelectors.values());
public @NotNull List<Selector> getSelectors() {
return new ArrayList<>(selectors.values());
}
public void setSelectors(@NotNull List<? extends Selector> selectors) {
mySelectors.clear();
public void setSelectors(@NotNull List<Selector> 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<Object> 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<Object> selectTasksList(@NotNull String response, int max) throws Exception;
protected abstract @NotNull List<Object> 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);

View File

@@ -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<Task> expected = List.of(new GenericTask("5479650606120", "Task #1", myRepository),