commit 4ca81518d56c925208f2da9ed381986e04eae514 Author: Alexander Nozik Date: Fri Sep 14 12:04:53 2018 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d482c058 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ + +.idea/ +*.iws +out/ +.gradle +/build/ + + +!gradle-wrapper.jar + diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..2c3d120e --- /dev/null +++ b/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'kotlin-platform-common' version '1.2.70' +} + +description = "The basic interfaces for DataForge meta-data" + +group 'hep.dataforge' +version '0.1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-common" + testCompile "org.jetbrains.kotlin:kotlin-test-annotations-common" + testCompile "org.jetbrains.kotlin:kotlin-test-common" +} +kotlin { + experimental { + coroutines "enable" + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..fa5906c2 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.id == "kotlin-platform-common") { + useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") + } + } + } +} +rootProject.name = 'dataforge-meta' + diff --git a/src/main/kotlin/hep/dataforge/meta/Meta.kt b/src/main/kotlin/hep/dataforge/meta/Meta.kt new file mode 100644 index 00000000..a322946f --- /dev/null +++ b/src/main/kotlin/hep/dataforge/meta/Meta.kt @@ -0,0 +1,71 @@ +package hep.dataforge.meta + +import hep.dataforge.names.Name +import hep.dataforge.names.NameToken +import hep.dataforge.names.toName + +/** + * A member of the meta tree. Could be represented as one of following: + * * a value + * * a single node + * * a list of nodes + */ +sealed class MetaItem> { + class ValueItem>(val value: Value) : MetaItem() + class SingleNodeItem>(val node: M) : MetaItem() + class MultiNodeItem>(val nodes: List) : MetaItem() +} + +operator fun > List.get(query: String): M? { + return if (query.isEmpty()) { + first() + } else { + //TODO add custom key queries + get(query.toInt()) + } +} + +/** + * Generic meta tree representation. Elements are [MetaItem] objects that could be represented by three different entities: + * * [MetaItem.ValueItem] (leaf) + * * [MetaItem.SingleNodeItem] single node + * * [MetaItem.MultiNodeItem] multi-value node + */ +interface Meta> { + val items: Map> + + operator fun get(name: Name): MetaItem? { + return when (name.length) { + 0 -> error("Can't resolve element from empty name") + 1 -> items[name.first()!!.body] + else -> name.first()!!.let{ token -> items[token.body]?.nodes?.get(token.query)}?.get(name.cutFirst()) + } + } + + operator fun get(key: String): MetaItem? = get(key.toName()) +} + +/** + * The meta implementation which is guaranteed to be immutable. + * + * If the argument is possibly mutable node, it is copied on creation + */ +class SealedMeta(meta: Meta<*>) : Meta { + override val items: Map> = if (meta is SealedMeta) { + meta.items + } else { + meta.items.mapValues { entry -> + val item = entry.value + when (item) { + is MetaItem.ValueItem -> MetaItem.ValueItem(item.value) + is MetaItem.SingleNodeItem -> MetaItem.SingleNodeItem(SealedMeta(item.node)) + is MetaItem.MultiNodeItem -> MetaItem.MultiNodeItem(item.nodes.map { SealedMeta(it) }) + } + } + } +} + +/** + * Generate sealed node from [this]. If it is already sealed return it as is + */ +fun Meta<*>.seal(): SealedMeta = this as? SealedMeta ?: SealedMeta(this) \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/MetaBuilder.kt b/src/main/kotlin/hep/dataforge/meta/MetaBuilder.kt new file mode 100644 index 00000000..8a6cdb9d --- /dev/null +++ b/src/main/kotlin/hep/dataforge/meta/MetaBuilder.kt @@ -0,0 +1,33 @@ +package hep.dataforge.meta + +/** + * DSL builder for meta + */ +class MetaBuilder : MutableMeta() { + override fun wrap(meta: Meta<*>): MetaBuilder = meta.builder() + override fun empty(): MetaBuilder = MetaBuilder() + + infix fun String.to(value: Any) { + this@MetaBuilder[this] = Value.of(value) + } + + infix fun String.to(metaBuilder: MetaBuilder.() -> Unit) { + this@MetaBuilder[this] = MetaBuilder().apply(metaBuilder) + } +} + +/** + * For safety, builder always copies the initial meta even if it is builder itself + */ +fun Meta<*>.builder(): MetaBuilder { + return MetaBuilder().also { builder -> + items.mapValues { entry -> + val item = entry.value + builder[entry.key] = when (item) { + is MetaItem.ValueItem -> MetaItem.ValueItem(item.value) + is MetaItem.SingleNodeItem -> MetaItem.SingleNodeItem(item.node.builder()) + is MetaItem.MultiNodeItem -> MetaItem.MultiNodeItem(item.nodes.map { it.builder() }) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/MetaUtils.kt b/src/main/kotlin/hep/dataforge/meta/MetaUtils.kt new file mode 100644 index 00000000..baafde2a --- /dev/null +++ b/src/main/kotlin/hep/dataforge/meta/MetaUtils.kt @@ -0,0 +1,40 @@ +package hep.dataforge.meta + +/** + * Unsafe methods to access values and nodes directly from [MetaItem] + */ + +val MetaItem<*>.value + get() = (this as? MetaItem.ValueItem)?.value ?: error("Trying to interpret node meta item as value item") +val MetaItem<*>.string get() = value.string +val MetaItem<*>.boolean get() = value.boolean +val MetaItem<*>.number get() = value.number +val MetaItem<*>.double get() = number.toDouble() +val MetaItem<*>.int get() = number.toInt() +val MetaItem<*>.long get() = number.toLong() + +val > MetaItem.node: M + get() = when (this) { + is MetaItem.ValueItem -> error("Trying to interpret value meta item as node item") + is MetaItem.SingleNodeItem -> node + is MetaItem.MultiNodeItem -> nodes.first() + } + +/** + * Utility method to access item content as list of nodes. + * Returns empty list if it is value item. + */ +val > MetaItem.nodes: List + get() = when (this) { + is MetaItem.ValueItem -> emptyList()//error("Trying to interpret value meta item as node item") + is MetaItem.SingleNodeItem -> listOf(node) + is MetaItem.MultiNodeItem -> nodes + } + +fun > MetaItem.indexOf(meta: M): Int { + return when (this) { + is MetaItem.ValueItem -> -1 + is MetaItem.SingleNodeItem -> if (node == meta) 0 else -1 + is MetaItem.MultiNodeItem -> nodes.indexOf(meta) + } +} \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/MutableMeta.kt b/src/main/kotlin/hep/dataforge/meta/MutableMeta.kt new file mode 100644 index 00000000..80c2e6d5 --- /dev/null +++ b/src/main/kotlin/hep/dataforge/meta/MutableMeta.kt @@ -0,0 +1,93 @@ +package hep.dataforge.meta + +import hep.dataforge.names.Name +import hep.dataforge.names.plus +import hep.dataforge.names.toName + +class MetaListener(val owner: Any? = null, val action: (name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) -> Unit) { + operator fun invoke(name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) = action(name, oldItem, newItem) +} + +/** + * A mutable meta node with attachable change listener + */ +abstract class MutableMeta> : Meta { + private val listeners = HashSet() + + /** + * Add change listener to this meta. Owner is declared to be able to remove listeners later. Listener without owner could not be removed + */ + fun onChange(owner: Any? = null, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) { + listeners.add(MetaListener(owner, action)) + } + + /** + * Remove all listeners belonging to given owner + */ + fun removeListener(owner: Any) { + listeners.removeAll { it.owner === owner } + } + + private val _items: MutableMap> = HashMap() + + override val items: Map> + get() = _items + + private fun itemChanged(name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) { + listeners.forEach { it(name, oldItem, newItem) } + } + + protected open fun replaceItem(key: String, oldItem: MetaItem?, newItem: MetaItem?) { + if (newItem == null) { + _items.remove(key) + oldItem?.nodes?.forEach { + it.removeListener(this) + } + } else { + _items[key] = newItem + newItem.nodes.forEach { + it.onChange(this) { name, oldItem, newItem -> + itemChanged(key.toName() + name, oldItem, newItem) + } + } + } + itemChanged(key.toName(), oldItem, newItem) + } + + /** + * Transform given meta to node type of this meta tree + */ + protected abstract fun wrap(meta: Meta<*>): M + + /** + * Create empty node + */ + protected abstract fun empty(): M + + operator fun set(name: Name, item: MetaItem) { + when (name.length) { + 0 -> error("Can't set meta item for empty name") + 1 -> { + val token = name.first()!! + if (token.hasQuery()) TODO("Queries are not supported in set operations on meta") + replaceItem(token.body, get(name), item) + } + else -> { + val token = name.first()!! + //get existing or create new node. Query is ignored for new node + val child = this.items[token.body]?.nodes?.get(token.query) + ?: empty().also { this[token.body.toName()] = MetaItem.SingleNodeItem(it) } + child[name.cutFirst()] = item + } + } + } + + operator fun set(name: Name, value: Value) = set(name, MetaItem.ValueItem(value)) + operator fun set(name: Name, meta: Meta<*>) = set(name, MetaItem.SingleNodeItem(wrap(meta))) + operator fun set(name: Name, metas: List>) = set(name, MetaItem.MultiNodeItem(metas.map { wrap(it) })) + + operator fun set(name: String, item: MetaItem) = set(name.toName(), item) + operator fun set(name: String, value: Value) = set(name.toName(), MetaItem.ValueItem(value)) + operator fun set(name: String, meta: Meta<*>) = set(name.toName(), MetaItem.SingleNodeItem(wrap(meta))) + operator fun set(name: String, metas: List>) = set(name.toName(), MetaItem.MultiNodeItem(metas.map { wrap(it) })) +} \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/Value.kt b/src/main/kotlin/hep/dataforge/meta/Value.kt new file mode 100644 index 00000000..4bd6d55e --- /dev/null +++ b/src/main/kotlin/hep/dataforge/meta/Value.kt @@ -0,0 +1,181 @@ +package hep.dataforge.meta + + +/** + * The list of supported Value types. + * + * Time value and binary value are represented by string + * + */ +enum class ValueType { + NUMBER, STRING, BOOLEAN, NULL +} + +/** + * A wrapper class for both Number and non-Number objects. + * + * Value can represent a list of value objects. + */ +interface Value { + /** + * Get raw value of this value + */ + val value: Any? + + /** + * The type of the value + */ + val type: ValueType + + /** + * get this value represented as Number + */ + val number: Number + + /** + * get this value represented as String + */ + val string: String + + /** + * get this value represented as List + */ + val list: List + get() = listOf(this) + + companion object { + /** + * Convert object to value + */ + fun of(value: Any?): Value { + return when (value) { + null -> Null + true -> True + false -> False + is Number -> NumberValue(value) + is Iterable<*> -> ListValue(value.map { of(it) }) + is Enum<*> -> EnumValue(value) + is CharSequence -> StringValue(value.toString()) + else -> throw IllegalArgumentException("Unrecognized type of the object converted to Value") + } + } + } +} + +/** + * A singleton null value + */ +object Null : Value { + override val value: Any? = null + override val type: ValueType = ValueType.NULL + override val number: Number = Double.NaN + override val string: String = "@null" +} + +/** + * Check if value is null + */ +fun Value.isNull(): Boolean = this == Null + + +/** + * Singleton true value + */ +object True : Value { + override val value: Any? = true + override val type: ValueType = ValueType.BOOLEAN + override val number: Number = 1.0 + override val string: String = "+" +} + +/** + * Singleton false value + */ +object False : Value { + override val value: Any? = false + override val type: ValueType = ValueType.BOOLEAN + override val number: Number = -1.0 + override val string: String = "-" +} + +val Value.boolean get() = this == True || this.list.firstOrNull() == True || (type == ValueType.STRING && string.toBoolean()) + +class NumberValue(override val number: Number) : Value { + override val value: Any? get() = number + override val type: ValueType = ValueType.NUMBER + override val string: String get() = number.toString() +} + +class StringValue(override val string: String) : Value { + override val value: Any? get() = string + override val type: ValueType = ValueType.STRING + override val number: Number get() = string.toDouble() +} + +class EnumValue>(override val value: E) : Value { + override val type: ValueType = ValueType.STRING + override val number: Number = value.ordinal + override val string: String = value.name +} + +class ListValue(override val list: List) : Value { + init { + if (list.isEmpty()) { + throw IllegalArgumentException("Can't create list value from empty list") + } + } + + override val value: Any? get() = list + override val type: ValueType get() = list.first().type + override val number: Number get() = list.first().number + override val string: String get() = list.first().string +} + +/** + * Check if value is list + */ +fun Value.isList(): Boolean = this.list.size > 1 + + +fun Number.asValue(): Value = NumberValue(this) + +fun Boolean.asValue(): Value = if (this) True else False + +fun String.asValue(): Value = StringValue(this) + +fun Collection.asValue(): Value = ListValue(this.toList()) + +/** + * Create Value from String using closest match conversion + */ +fun String.parseValue(): Value { + + //Trying to get integer + if (isEmpty() || this == Null.string) { + return Null + } + + //string constants + if (startsWith("\"") && endsWith("\"")) { + return StringValue(substring(1, length - 2)) + } + + toIntOrNull()?.let { + return NumberValue(it) + } + + toDoubleOrNull()?.let { + return NumberValue(it) + } + + if ("true" == this) { + return True + } + + if ("false" == this) { + return False + } + + //Give up and return a StringValue + return StringValue(this) +} \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/names/Name.kt b/src/main/kotlin/hep/dataforge/names/Name.kt new file mode 100644 index 00000000..233582fb --- /dev/null +++ b/src/main/kotlin/hep/dataforge/names/Name.kt @@ -0,0 +1,112 @@ +package hep.dataforge.names + +import kotlin.coroutines.experimental.buildSequence + +/** + * The general interface for working with names. + * The name is a dot separated list of strings like `token1.token2.token3`. + * Each token could contain additional query in square brackets. + */ +class Name internal constructor(val tokens: List) { + + val length + get() = tokens.size + + /** + * First token of the name or null if it is empty + */ + fun first(): NameToken? = tokens.firstOrNull() + + /** + * Last token of the name or null if it is empty + */ + fun last(): NameToken? = tokens.lastOrNull() + + /** + * The reminder of the name after first element is cut + */ + fun cutFirst(): Name = Name(tokens.drop(1)) + + /** + * The reminder of the name after last element is cut + */ + fun cutLast(): Name = Name(tokens.dropLast(1)) + + operator fun get(i: Int): NameToken = tokens[i] + + override fun toString(): String = tokens.joinToString(separator = NAME_SEPARATOR) { it.toString() } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Name) return false + + if (tokens != other.tokens) return false + + return true + } + + override fun hashCode(): Int { + return tokens.hashCode() + } + + companion object { + const val NAME_SEPARATOR = "." + } +} + +/** + * A single name token. Body is not allowed to be empty. + * Following symbols are prohibited in name tokens: `{}.:\`. + * A name token could have appendix in square brackets called *query* + */ +data class NameToken internal constructor(val body: String, val query: String) { + + init { + if (body.isEmpty()) error("Syntax error: Name token body is empty") + } + + override fun toString(): String = "$body[$query]" + + fun hasQuery() = query.isNotEmpty() +} + +fun String.toName(): Name { + val tokens = buildSequence { + var bodyBuilder = StringBuilder() + var queryBuilder = StringBuilder() + var bracketCount: Int = 0 + fun queryOn() = bracketCount > 0 + + this@toName.asSequence().forEach { + if (queryOn()) { + when (it) { + '[' -> bracketCount++ + ']' -> bracketCount-- + } + if (queryOn()) queryBuilder.append(it) + } else { + when (it) { + '.' -> { + yield(NameToken(bodyBuilder.toString(), queryBuilder.toString())) + bodyBuilder = StringBuilder() + queryBuilder = StringBuilder() + } + '[' -> bracketCount++ + ']' -> error("Syntax error: closing bracket ] not have not matching open bracket") + else -> { + if (queryBuilder.isNotEmpty()) error("Syntax error: only name end and name separator are allowed after query") + bodyBuilder.append(it) + } + } + } + } + yield(NameToken(bodyBuilder.toString(), queryBuilder.toString())) + } + return Name(tokens.toList()) +} + +operator fun Name.plus(other: Name): Name = Name(this.tokens + other.tokens) + +operator fun Name.plus(other: String): Name = this + other.toName() + +fun NameToken.toName() = Name(listOf(this)) \ No newline at end of file diff --git a/src/test/kotlin/hep/dataforge/names/NameTest.kt b/src/test/kotlin/hep/dataforge/names/NameTest.kt new file mode 100644 index 00000000..3d4a13b9 --- /dev/null +++ b/src/test/kotlin/hep/dataforge/names/NameTest.kt @@ -0,0 +1,12 @@ +package hep.dataforge.names + +import kotlin.test.Test +import kotlin.test.assertEquals + +class NameTest{ + @Test + fun simpleName(){ + val name = "token1.token2.token3".toName() + assertEquals("token2", name[2].toString()) + } +} \ No newline at end of file