fix copyFrom and add test, use kotlin-friendly xml serializer to serialize component states

This commit is contained in:
Vladimir Krivosheev
2017-05-19 14:32:23 +02:00
parent 26d2f1160e
commit d739492020
10 changed files with 197 additions and 138 deletions
@@ -20,9 +20,6 @@ import com.intellij.openapi.util.JDOMExternalizable
import com.intellij.openapi.util.WriteExternalException
import com.intellij.openapi.vfs.LargeFileWriteRequestor
import com.intellij.openapi.vfs.SafeWriteRequestor
import com.intellij.reference.SoftReference
import com.intellij.util.xmlb.SkipDefaultsSerializationFilter
import com.intellij.util.xmlb.XmlSerializer
import org.jdom.Element
abstract class SaveSessionBase : StateStorage.SaveSession, StateStorage.ExternalizationSession, SafeWriteRequestor, LargeFileWriteRequestor {
@@ -46,9 +43,8 @@ abstract class SaveSessionBase : StateStorage.SaveSession, StateStorage.External
protected abstract fun setSerializedState(componentName: String, element: Element?)
}
private val skipDefaultsSerializationFilter = ThreadLocal<SoftReference<SkipDefaultsSerializationFilter>>()
fun serializeState(state: Any): Element? {
internal fun serializeState(state: Any): Element? {
@Suppress("DEPRECATION")
if (state is Element) {
return state
}
@@ -58,11 +54,6 @@ fun serializeState(state: Any): Element? {
return element
}
else {
var serializationFilter = SoftReference.dereference(skipDefaultsSerializationFilter.get())
if (serializationFilter == null) {
serializationFilter = SkipDefaultsSerializationFilter()
skipDefaultsSerializationFilter.set(SoftReference(serializationFilter))
}
return XmlSerializer.serializeIfNotDefault(state, serializationFilter)
return state.serialize()
}
}
@@ -18,6 +18,7 @@ package com.intellij.configurationStore
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.util.JDOMUtil
import com.intellij.util.isEmpty
import org.jdom.Element
abstract class StorageBaseEx<T : Any> : StateStorageBase<T>() {
@@ -61,7 +62,7 @@ class StateGetter<S : Any, T : Any>(private val component: PersistentStateCompon
}
else {
serializeState(stateAfterLoad)?.normalizeRootName().let {
if (JDOMUtil.isEmpty(it)) null else it
if (it.isEmpty()) null else it
}
}
@@ -143,7 +143,7 @@ internal class SchemeManagerTest {
}
fun TestScheme.save(file: Path) {
file.write(serialize().toByteArray())
file.write(serialize()!!.toByteArray())
}
@Test fun `different extensions`() {
@@ -429,7 +429,7 @@ data class TestScheme(@field:com.intellij.util.xmlb.annotations.Attribute @field
name = value
}
override fun writeScheme() = serialize()
override fun writeScheme() = serialize()!!
}
open class TestSchemesProcessor : LazySchemeProcessor<TestScheme, TestScheme>() {
@@ -6,15 +6,17 @@ import com.intellij.util.loadElement
import com.intellij.util.xmlb.annotations.Attribute
import org.junit.Test
internal class AState : BaseState() {
internal class AState(languageLevel: String? = null, nestedComplex: NestedState? = null) : BaseState() {
@get:Attribute("customName")
var languageLevel by storedProperty<String?>()
var languageLevel by storedProperty<String?>(languageLevel)
var bar by string()
var property2 by storedProperty(0)
var floatProperty by storedProperty(0.3)
var nestedComplex by storedProperty<NestedState?>()
var nestedComplex by storedProperty<NestedState?>(nestedComplex)
}
internal class NestedState : BaseState() {
@@ -37,6 +39,10 @@ class StoredPropertyStateTest {
assertThat(state).isNotEqualTo(AState())
val newEqualState = AState()
newEqualState.languageLevel = String("foo".toCharArray())
assertThat(state).isEqualTo(newEqualState)
assertThat(state.serialize()).isEqualTo("""<AState customName="foo" />""")
assertThat(loadElement("""<AState customName="foo" />""").deserialize(AState::class.java).languageLevel).isEqualTo("foo")
}
@@ -57,5 +63,10 @@ class StoredPropertyStateTest {
state.languageLevel = null
assertThat(state.modificationCount).isEqualTo(4)
state.copyFrom(AState("foo", nestedState))
@Suppress("USELESS_CAST")
assertThat(state.languageLevel as String?).isEqualTo("foo")
assertThat(state.modificationCount).isEqualTo(5)
}
}
@@ -242,7 +242,7 @@ internal class XmlSerializerTest {
bean.INT_V = 987
bean.STRING_V = "1234"
val element = bean.serialize()
val element = bean.serialize()!!
val node = element.children.get(0)
element.removeContent(node)
@@ -660,8 +660,8 @@ internal class XmlSerializerTest {
private val XML_PREFIX = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
internal fun assertSerializer(bean: Any, expected: String, filter: SerializationFilter?, description: String = "Serialization failure"): Element {
val element = bean.serialize(filter)
var actual = JDOMUtil.writeElement(element, "\n").trim()
val element = bean.serialize(filter, createElementIfEmpty = true)!!
var actual = JDOMUtil.writeElement(element).trim()
if (!expected.startsWith(XML_PREFIX) && actual.startsWith(XML_PREFIX)) {
actual = actual.substring(XML_PREFIX.length).trim()
}
@@ -3,32 +3,31 @@ package com.intellij.configurationStore.xml;
import com.intellij.configurationStore.XmlSerializer;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.xmlb.SerializationFilter;
import com.intellij.util.xmlb.annotations.Transient;
import junit.framework.TestCase;
import org.jdom.Element;
import org.jetbrains.annotations.Nullable;
import static com.intellij.testFramework.assertions.Assertions.assertThat;
@SuppressWarnings({"deprecation"})
public class XmlSerializerWithDefaultJDOMExternalizerCompatibilityTest extends TestCase {
public void testCompatibility() throws Exception {
final MyBean bean = new MyBean();
assertCompatibleSerialization(bean, "MyBean");
assertCompatibleSerialization(new MyBean());
}
private static void assertCompatibleSerialization(final Object data, final String rootTagName) throws Exception {
assertEquals(serializeWithJDom(data, rootTagName), serializeWithXmlSerializer(data));
private static void assertCompatibleSerialization(final Object data) throws Exception {
assertThat(serializeWithJDom(data)).isEqualTo(serializeWithXmlSerializer(data));
}
private static String serializeWithXmlSerializer(final Object data) throws Exception {
Element element = serialize(data, null);
Element element = serialize(data);
String s = JDOMUtil.writeElement(element, "\n");
return StringUtil.trimStart(s, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>").trim();
}
private static String serializeWithJDom(final Object data, String rootTagName) throws Exception {
final Element jDomRoot = new Element(rootTagName);
private static String serializeWithJDom(final Object data) throws Exception {
final Element jDomRoot = new Element("MyBean");
if (data instanceof com.intellij.openapi.util.JDOMExternalizable) {
((com.intellij.openapi.util.JDOMExternalizable)data).writeExternal(jDomRoot);
@@ -37,11 +36,11 @@ public class XmlSerializerWithDefaultJDOMExternalizerCompatibilityTest extends T
com.intellij.openapi.util.DefaultJDOMExternalizer.writeExternal(data, jDomRoot);
}
return JDOMUtil.writeElement(jDomRoot, "\n").trim();
return JDOMUtil.writeElement(jDomRoot).trim();
}
private static Element serialize(Object bean, @Nullable SerializationFilter filter) {
return XmlSerializer.serialize(bean, filter);
private static Element serialize(Object bean) {
return XmlSerializer.serialize(bean, null);
}
@SuppressWarnings("unused")
@@ -20,23 +20,56 @@ import com.intellij.util.SmartList
import com.intellij.util.xmlb.Accessor
import com.intellij.util.xmlb.SerializationFilter
import com.intellij.util.xmlb.annotations.Transient
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
abstract class BaseState : SerializationFilter, ModificationTracker {
// if property value differs from default
private val properties: MutableList<StoredProperty> = SmartList()
@Volatile
internal var modificationCount: Long = 0
@Transient
@JvmField
internal var ownModificationCount: Long = 0
// reset on load state
fun resetModificationCount() {
modificationCount = 0
ownModificationCount = 0
}
protected fun incrementModificationCount() {
modificationCount++
ownModificationCount++
}
fun <T> storedProperty(defaultValue: T? = null): StoredPropertyBase<T?> {
val result = ObjectStoredProperty(defaultValue)
properties.add(result)
return result
}
/**
* Empty string is always normalized to null.
*/
fun string(defaultValue: String? = null): StoredPropertyBase<String?> {
val result = NormalizedStringStoredProperty(defaultValue)
properties.add(result)
return result
}
fun storedProperty(defaultValue: Int = 0): StoredPropertyBase<Int> {
val result = IntStoredProperty(defaultValue)
properties.add(result)
return result
}
fun storedProperty(defaultValue: Float = 0f): StoredPropertyBase<Float> {
val result = FloatStoredProperty(defaultValue)
properties.add(result)
return result
}
fun storedProperty(defaultValue: Boolean = false): StoredPropertyBase<Boolean> {
val result = ObjectStoredProperty(defaultValue)
properties.add(result)
return result
}
override fun accepts(accessor: Accessor, bean: Any): Boolean {
@@ -48,42 +81,9 @@ abstract class BaseState : SerializationFilter, ModificationTracker {
return false
}
fun <T> storedProperty(defaultValue: T? = null): ReadWriteProperty<BaseState, T?> {
val result = ObjectStoredProperty(defaultValue)
properties.add(result)
return result
}
/**
* Empty string is always normalized to null.
*/
fun string(defaultValue: String? = null): ReadWriteProperty<BaseState, String?> {
val result = StringStoredProperty(defaultValue)
properties.add(result)
return result
}
fun storedProperty(defaultValue: Int = 0): ReadWriteProperty<BaseState, Int> {
val result = IntStoredProperty(defaultValue)
properties.add(result)
return result
}
fun storedProperty(defaultValue: Float = 0f): ReadWriteProperty<BaseState, Float> {
val result = FloatStoredProperty(defaultValue)
properties.add(result)
return result
}
fun storedProperty(defaultValue: Boolean = false): ReadWriteProperty<BaseState, Boolean> {
val result = ObjectStoredProperty(defaultValue)
properties.add(result)
return result
}
@Transient
override fun getModificationCount(): Long {
var result = modificationCount
var result = ownModificationCount
for (property in properties) {
val value = property.value
if (value is ModificationTracker) {
@@ -112,8 +112,10 @@ abstract class BaseState : SerializationFilter, ModificationTracker {
fun copyFrom(state: BaseState) {
assert(state.properties.size == properties.size)
var changed = false
for ((index, property) in properties.withIndex()) {
val otherProperty = state.properties.get(index)
//assert(otherProperty.name == property.name)
if (property.name != null) {
if (otherProperty.name == null) {
otherProperty.name = property.name
@@ -122,37 +124,30 @@ abstract class BaseState : SerializationFilter, ModificationTracker {
assert(otherProperty.name == property.name)
}
}
property.setValue(otherProperty)
if (property.setValue(otherProperty)) {
changed = true
}
}
if (changed) {
incrementModificationCount()
}
}
}
internal interface StoredProperty {
val defaultValue: Any?
val value: Any?
var name: String?
fun setValue(other: StoredProperty)
}
private class ObjectStoredProperty<T>(override val defaultValue: T) : ReadWriteProperty<BaseState, T>, StoredProperty {
private class ObjectStoredProperty<T>(override val defaultValue: T) : StoredPropertyBase<T>() {
override var value = defaultValue
override var name: String? = null
override operator fun getValue(thisRef: BaseState, property: KProperty<*>): T {
name = property.name
return value
}
@Suppress("UNCHECKED_CAST")
override fun setValue(thisRef: BaseState, property: KProperty<*>, @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") newValue: T) {
override fun setValue(thisRef: BaseState, property: KProperty<*>, value: T) {
name = property.name
if (value != newValue) {
thisRef.modificationCount++
value = newValue
if (this.value != value) {
thisRef.ownModificationCount++
this.value = value
}
}
@@ -160,66 +155,71 @@ private class ObjectStoredProperty<T>(override val defaultValue: T) : ReadWriteP
override fun hashCode() = value?.hashCode() ?: 0
override fun toString() = if (value === defaultValue) "" else value?.toString() ?: super.toString()
override fun toString() = if (value == defaultValue) "" else value?.toString() ?: super.toString()
override fun setValue(other: StoredProperty) {
override fun setValue(other: StoredProperty): Boolean {
@Suppress("UNCHECKED_CAST")
value = (other as ObjectStoredProperty<T>).value
val newValue = (other as ObjectStoredProperty<T>).value
if (newValue == value) {
return false
}
value = newValue
return true
}
}
private class StringStoredProperty(override val defaultValue: String?) : ReadWriteProperty<BaseState, String?>, StoredProperty {
private class NormalizedStringStoredProperty(override val defaultValue: String?) : StoredPropertyBase<String?>() {
override var value = defaultValue
override var name: String? = null
override operator fun getValue(thisRef: BaseState, property: KProperty<*>): String? {
name = property.name
return value
}
@Suppress("UNCHECKED_CAST")
override fun setValue(thisRef: BaseState, property: KProperty<*>, @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") _newValue: String?) {
override fun setValue(thisRef: BaseState, property: KProperty<*>, value: String?) {
name = property.name
var newValue = _newValue
var newValue = value
if (newValue != null && newValue.isEmpty()) {
newValue = null
}
if (value != newValue) {
thisRef.modificationCount++
value = newValue
if (this.value != newValue) {
thisRef.ownModificationCount++
this.value = newValue
}
}
override fun equals(other: Any?) = this === other || (other is StringStoredProperty && value == other.value)
override fun equals(other: Any?) = this === other || (other is NormalizedStringStoredProperty && value == other.value)
override fun hashCode() = value?.hashCode() ?: 0
override fun toString() = if (value == defaultValue) "" else value ?: super.toString()
override fun setValue(other: StoredProperty) {
value = (other as StringStoredProperty).value
override fun setValue(other: StoredProperty): Boolean {
val newValue = (other as NormalizedStringStoredProperty).value
if (newValue == value) {
return false
}
value = newValue
return true
}
}
private class IntStoredProperty(override val defaultValue: Int) : ReadWriteProperty<BaseState, Int>, StoredProperty {
private class IntStoredProperty(override val defaultValue: Int) : StoredPropertyBase<Int>() {
override var value = defaultValue
override var name: String? = null
override operator fun getValue(thisRef: BaseState, property: KProperty<*>): Int {
name = property.name
return value
}
@Suppress("UNCHECKED_CAST")
override fun setValue(thisRef: BaseState, property: KProperty<*>, @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") newValue: Int) {
override fun setValue(thisRef: BaseState, property: KProperty<*>, value: Int) {
name = property.name
if (value != newValue) {
thisRef.modificationCount++
value = newValue
if (this.value != value) {
thisRef.ownModificationCount++
this.value = value
}
}
@@ -229,28 +229,30 @@ private class IntStoredProperty(override val defaultValue: Int) : ReadWritePrope
override fun toString() = if (value == defaultValue) "" else value.toString()
override fun setValue(other: StoredProperty) {
value = (other as IntStoredProperty).value
override fun setValue(other: StoredProperty): Boolean {
val newValue = (other as IntStoredProperty).value
if (newValue == value) {
return false
}
value = newValue
return true
}
}
private class FloatStoredProperty(override val defaultValue: Float) : ReadWriteProperty<BaseState, Float>, StoredProperty {
private class FloatStoredProperty(override val defaultValue: Float) : StoredPropertyBase<Float>() {
override var value = defaultValue
override var name: String? = null
override operator fun getValue(thisRef: BaseState, property: KProperty<*>): Float {
name = property.name
return value
}
@Suppress("UNCHECKED_CAST")
override fun setValue(thisRef: BaseState, property: KProperty<*>, @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") newValue: Float) {
override fun setValue(thisRef: BaseState, property: KProperty<*>, value: Float) {
name = property.name
if (value != newValue) {
thisRef.modificationCount++
value = newValue
if (this.value != value) {
thisRef.ownModificationCount++
this.value = value
}
}
@@ -260,7 +262,13 @@ private class FloatStoredProperty(override val defaultValue: Float) : ReadWriteP
override fun toString() = if (value == defaultValue) "" else value.toString()
override fun setValue(other: StoredProperty) {
value = (other as FloatStoredProperty).value
override fun setValue(other: StoredProperty): Boolean {
val newValue = (other as FloatStoredProperty).value
if (newValue == value) {
return false
}
value = newValue
return true
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2000-2017 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.
*/
package com.intellij.openapi.components
import kotlin.properties.ReadWriteProperty
internal interface StoredProperty {
val defaultValue: Any?
val value: Any?
var name: String?
// true if changed
fun setValue(other: StoredProperty): Boolean
}
// type must be exposed otherwise `provideDelegate` doesn't work
abstract class StoredPropertyBase<T> : ReadWriteProperty<BaseState, T>, StoredProperty {
override final var name: String? = null
// operator fun provideDelegate(thisRef: Any, property: KProperty<*>): ReadWriteProperty<BaseState, T> {
// name = property.name
// return this
// }
}
@@ -34,14 +34,25 @@ import kotlin.concurrent.write
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.primaryConstructor
private val skipDefaultsSerializationFilter = ThreadLocal<SoftReference<SkipDefaultsSerializationFilter>>()
private fun getDefaultSerializationFilter(): SkipDefaultsSerializationFilter {
var result = SoftReference.dereference(skipDefaultsSerializationFilter.get())
if (result == null) {
result = SkipDefaultsSerializationFilter()
skipDefaultsSerializationFilter.set(SoftReference(result))
}
return result
}
@JvmOverloads
fun <T : Any> T.serialize(filter: SerializationFilter? = SkipDefaultsSerializationFilter()): Element {
fun <T : Any> T.serialize(filter: SerializationFilter? = getDefaultSerializationFilter(), createElementIfEmpty: Boolean = false): Element? {
try {
val clazz = javaClass
val binding = serializer.getClassBinding(clazz)
return if (binding is BeanBinding) {
// top level expects not null (null indicates error, empty element will be omitted)
binding.serialize(this, true, filter)
binding.serialize(this, createElementIfEmpty, filter)
}
else {
binding.serialize(this, null, filter) as Element
@@ -42,7 +42,7 @@ class LoadTest : IcsTestCase() {
@Test fun `load scheme`() {
val localScheme = TestScheme("local")
provider.write("$dirName/local.xml", localScheme.serialize().toByteArray())
provider.write("$dirName/local.xml", localScheme.serialize()!!.toByteArray())
val schemeManager = createSchemeManager(dirName)
schemeManager.loadSchemes()
@@ -58,8 +58,8 @@ class LoadTest : IcsTestCase() {
assertThat(dirPath).doesNotExist()
provider.write("$dirName/local1.xml", TestScheme("local1").serialize().toByteArray())
provider.write("$dirName/local2.xml", TestScheme("local2").serialize().toByteArray())
provider.write("$dirName/local1.xml", TestScheme("local1").serialize()!!.toByteArray())
provider.write("$dirName/local2.xml", TestScheme("local2").serialize()!!.toByteArray())
assertThat(dirPath.resolve("local1.xml")).isRegularFile()
assertThat(dirPath.resolve("local2.xml")).isRegularFile()
@@ -74,7 +74,7 @@ class LoadTest : IcsTestCase() {
@Test fun `load scheme with the same names`() {
val localScheme = TestScheme("local")
val data = localScheme.serialize().toByteArray()
val data = localScheme.serialize()!!.toByteArray()
provider.write("$dirName/local.xml", data)
provider.write("$dirName/local2.xml", data)
@@ -86,12 +86,12 @@ class LoadTest : IcsTestCase() {
@Test fun `load scheme from repo and read-only repo`() {
val localScheme = TestScheme("local")
provider.write("$dirName/local.xml", localScheme.serialize().toByteArray())
provider.write("$dirName/local.xml", localScheme.serialize()!!.toByteArray())
val remoteScheme = TestScheme("remote")
val remoteRepository = tempDirManager.createRepository()
remoteRepository
.add("$dirName/Mac OS X from RubyMine.xml", remoteScheme.serialize().toByteArray())
.add("$dirName/Mac OS X from RubyMine.xml", remoteScheme.serialize()!!.toByteArray())
.commit("")
remoteRepository.useAsReadOnlySource {
@@ -114,12 +114,12 @@ class LoadTest : IcsTestCase() {
val schemeName = "Emacs"
val localScheme = TestScheme(schemeName, "local")
provider.write("$dirName/$schemeName.xml", localScheme.serialize().toByteArray())
provider.write("$dirName/$schemeName.xml", localScheme.serialize()!!.toByteArray())
val remoteScheme = TestScheme(schemeName, "remote")
val remoteRepository = tempDirManager.createRepository("remote")
remoteRepository
.add("$dirName/$schemeName.xml", remoteScheme.serialize().toByteArray())
.add("$dirName/$schemeName.xml", remoteScheme.serialize()!!.toByteArray())
.commit("")
remoteRepository.useAsReadOnlySource {