initial commit

This commit is contained in:
Alexander Nozik 2018-09-14 12:04:53 +03:00
commit 4ca81518d5
10 changed files with 586 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.idea/
*.iws
out/
.gradle
/build/
!gradle-wrapper.jar

23
build.gradle Normal file
View File

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

11
settings.gradle Normal file
View File

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

View File

@ -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<M : Meta<M>> {
class ValueItem<M : Meta<M>>(val value: Value) : MetaItem<M>()
class SingleNodeItem<M : Meta<M>>(val node: M) : MetaItem<M>()
class MultiNodeItem<M : Meta<M>>(val nodes: List<M>) : MetaItem<M>()
}
operator fun <M : Meta<M>> List<M>.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<M : Meta<M>> {
val items: Map<String, MetaItem<M>>
operator fun get(name: Name): MetaItem<M>? {
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<M>? = 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<SealedMeta> {
override val items: Map<String, MetaItem<SealedMeta>> = 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)

View File

@ -0,0 +1,33 @@
package hep.dataforge.meta
/**
* DSL builder for meta
*/
class MetaBuilder : MutableMeta<MetaBuilder>() {
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() })
}
}
}
}

View File

@ -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 <M : Meta<M>> MetaItem<M>.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 <M : Meta<M>> MetaItem<M>.nodes: List<M>
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 <M : Meta<M>> MetaItem<M>.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)
}
}

View File

@ -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<M : MutableMeta<M>> : Meta<M> {
private val listeners = HashSet<MetaListener>()
/**
* 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<String, MetaItem<M>> = HashMap()
override val items: Map<String, MetaItem<M>>
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<M>?, newItem: MetaItem<M>?) {
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<M>) {
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<Meta<*>>) = set(name, MetaItem.MultiNodeItem(metas.map { wrap(it) }))
operator fun set(name: String, item: MetaItem<M>) = 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<Meta<*>>) = set(name.toName(), MetaItem.MultiNodeItem(metas.map { wrap(it) }))
}

View File

@ -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<Value>
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<E : Enum<*>>(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>) : 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<Value>.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)
}

View File

@ -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<NameToken>) {
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<NameToken> {
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))

View File

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