Context module

This commit is contained in:
Alexander Nozik 2018-11-06 14:09:43 +03:00
parent dd05897759
commit d3ce88eb3f
33 changed files with 1151 additions and 65 deletions

View File

@ -20,9 +20,7 @@ allprojects {
repositories {
jcenter()
maven { url = "http://dl.bintray.com/kotlin/kotlin-eap" }
maven { url = "https://kotlin.bintray.com/kotlinx" }
//maven { url 'https://jitpack.io' }
}
group = 'hep.dataforge'

View File

@ -0,0 +1,32 @@
plugins {
id 'kotlin-multiplatform'
}
repositories {
jcenter()
}
kotlin {
targets {
fromPreset(presets.jvm, 'jvm')
//fromPreset(presets.js, 'js')
// For ARM, preset should be changed to presets.iosArm32 or presets.iosArm64
// For Linux, preset should be changed to e.g. presets.linuxX64
// For MacOS, preset should be changed to e.g. presets.macosX64
//fromPreset(presets.iosX64, 'ios')
}
sourceSets {
commonMain {
dependencies {
api project(":dataforge-meta")
api "org.jetbrains.kotlin:kotlin-reflect"
api "io.github.microutils:kotlin-logging-common:1.6.10"
}
}
jvmMain{
dependencies{
api "io.github.microutils:kotlin-logging:1.6.10"
}
}
}
}

View File

@ -0,0 +1,106 @@
package hep.dataforge.context
import hep.dataforge.meta.*
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.provider.Provider
import hep.dataforge.provider.provideAll
import hep.dataforge.values.Value
import mu.KLogger
import mu.KotlinLogging
import kotlin.reflect.KClass
interface Context : Named, MetaRepr, Provider {
val parent: Context?
/**
* Context properties. Working as substitutes for environment variables
*/
val properties: Meta
/**
* Context logger
*/
val logger: KLogger
val plugins: PluginManager
/**
* Defines if context is used in any kind of active computations. Active context properties and plugins could not be changed
*/
val isActive: Boolean
/**
* Provide services for given type
*/
fun <T : Any> services(type: KClass<T>): Sequence<T>
override val defaultTarget: String get() = Plugin.PLUGIN_TARGET
override fun provideTop(target: String, name: Name): Any? {
return when (target) {
Plugin.PLUGIN_TARGET -> plugins[PluginTag.fromString(name.toString())]
Value.VALUE_TARGET -> properties[name]?.value
else -> null
}
}
override fun listTop(target: String): Sequence<Name> {
return when (target) {
Plugin.PLUGIN_TARGET -> plugins.asSequence().map { it.name.toName() }
Value.VALUE_TARGET -> properties.asValueSequence().map { it.first }
else -> emptySequence()
}
}
/**
* Mark context as active and used by [activator]
*/
fun activate(activator: Any)
/**
* Mark context unused by [activator]
*/
fun deactivate(activator: Any)
/**
* Detach all plugins and terminate context
*/
fun close()
}
/**
* A sequences of all objects provided by plugins with given target and type
*/
inline fun <reified T : Any> Context.list(target: String): Sequence<T> {
return plugins.asSequence().flatMap { provideAll(target) }.mapNotNull { it as? T }
}
/**
* A global root context
*/
expect object Global : Context
/**
* The interface for something that encapsulated in context
*
* @author Alexander Nozik
* @version $Id: $Id
*/
interface ContextAware {
/**
* Get context for this object
*
* @return
*/
val context: Context
val logger: KLogger
get() = if (this is Named) {
KotlinLogging.logger(context.name + "." + (this as Named).name)
} else {
context.logger
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2015 Alexander Nozik.
*
* 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 hep.dataforge.context
/**
* Any object that have name
*
* @author Alexander Nozik
*/
interface Named {
/**
* The name of this object instance
*
* @return
*/
val name: String
companion object {
const val ANONYMOUS = ""
/**
* Get the name of given object. If object is Named its name is used,
* otherwise, use Object.toString
*
* @param obj
* @return
*/
fun nameOf(obj: Any): String {
return if (obj is Named) {
obj.name
} else {
obj.toString()
}
}
}
}
/**
* Check if this object has an empty name and therefore is anonymous.
* @return
*/
val Named.isAnonymous: Boolean
get() = this.name == Named.ANONYMOUS

View File

@ -0,0 +1,78 @@
package hep.dataforge.context
import hep.dataforge.meta.Meta
import hep.dataforge.meta.MetaRepr
import hep.dataforge.meta.Metoid
import hep.dataforge.meta.buildMeta
import hep.dataforge.provider.Provider
/**
* The interface to define a Context plugin. A plugin stores all runtime features of a context.
* The plugin is by default configurable and a Provider (both features could be ignored).
* The plugin must in most cases have an empty constructor in order to be able to load it from library.
*
*
* The plugin lifecycle is the following:
*
*
* create - configure - attach - detach - destroy
*
*
* Configuration of attached plugin is possible for a context which is not in a runtime mode, but it is not recommended.
*
* @author Alexander Nozik
*/
interface Plugin : Named, Metoid, ContextAware, Provider, MetaRepr {
/**
* Get tag for this plugin
*
* @return
*/
val tag: PluginTag
/**
* The name of this plugin ignoring version and group
*
* @return
*/
override val name: String
get() = tag.name
/**
* Plugin dependencies which are required to attach this plugin. Plugin
* dependencies must be initialized and enabled in the Context before this
* plugin is enabled.
*
* @return
*/
fun dependsOn(): List<PluginTag>
/**
* Start this plugin and attach registration info to the context. This method
* should be called only via PluginManager to avoid dependency issues.
*
* @param context
*/
fun attach(context: Context)
/**
* Stop this plugin and remove registration info from context and other
* plugins. This method should be called only via PluginManager to avoid
* dependency issues.
*/
fun detach()
override fun toMeta(): Meta = buildMeta {
"context" to context.name
"type" to this::class.qualifiedName
"tag" to tag
"meta" to meta
}
companion object {
const val PLUGIN_TARGET = "plugin"
}
}

View File

@ -0,0 +1,145 @@
package hep.dataforge.context
import hep.dataforge.meta.EmptyMeta
import hep.dataforge.meta.Meta
import hep.dataforge.meta.MetaBuilder
import hep.dataforge.meta.buildMeta
import kotlin.reflect.KClass
/**
* The manager for plugin system. Should monitor plugin dependencies and locks.
*
* @property context A context for this plugin manager
* @author Alexander Nozik
*/
class PluginManager(override val context: Context) : ContextAware, Iterable<Plugin> {
/**
* A set of loaded plugins
*/
private val plugins = HashSet<Plugin>()
private val parent: PluginManager? = context.parent?.plugins
fun sequence(recursive: Boolean): Sequence<Plugin> {
return if (recursive && parent != null) {
plugins.asSequence() + parent.sequence(true)
} else {
plugins.asSequence()
}
}
/**
* Get for existing plugin
*/
fun get(recursive: Boolean = true, predicate: (Plugin) -> Boolean): Plugin? = sequence(recursive).find(predicate)
/**
* Find a loaded plugin via its tag
*
* @param tag
* @return
*/
operator fun get(tag: PluginTag, recursive: Boolean = true): Plugin? = get(recursive) { tag.matches(it.tag) }
/**
* Find a loaded plugin via its class
*
* @param tag
* @param type
* @param <T>
* @return
*/
@Suppress("UNCHECKED_CAST")
operator fun <T : Plugin> get(type: KClass<T>, recursive: Boolean = true): T? = get(recursive) { type.isInstance(it) } as T?
inline fun <reified T : Plugin> get(recursive: Boolean = true): T? = get(T::class, recursive)
/**
* Load given plugin into this manager and return loaded instance.
* Throw error if plugin of the same class already exists in manager
*
* @param plugin
* @return
*/
fun <T : Plugin> load(plugin: T): T {
if (context.isActive) error("Can't load plugin into active context")
if (get(plugin::class, false) != null) {
throw RuntimeException("Plugin of type ${plugin::class} already exists in ${context.name}")
} else {
loadDependencies(plugin)
logger.info { "Loading plugin ${plugin.name} into ${context.name}" }
plugin.attach(context)
plugins.add(plugin)
return plugin
}
}
private fun loadDependencies(plugin: Plugin) {
for (tag in plugin.dependsOn()) {
load(tag)
}
}
fun remove(plugin: Plugin) {
if (context.isActive) error("Can't remove plugin from active context")
if (plugins.contains(plugin)) {
logger.info { "Removing plugin ${plugin.name} from ${context.name}" }
plugin.detach()
plugins.remove(plugin)
}
}
/**
* Get plugin instance via plugin reolver and load it.
*
* @param tag
* @return
*/
fun load(tag: PluginTag, meta: Meta = EmptyMeta): Plugin {
val loaded = get(tag, false)
return when {
loaded == null -> load(PluginRepository.fetch(tag, meta))
loaded.meta == meta -> loaded // if meta is the same, return existing plugin
else -> throw RuntimeException("Can't load plugin with tag $tag. Plugin with this tag and different configuration already exists in context.")
}
}
/**
* Load plugin by its class and meta. Ignore if plugin with this meta is already loaded.
*/
fun <T : Plugin> load(type: KClass<T>, meta: Meta = EmptyMeta): T {
val loaded = get(type, false)
return when {
loaded == null -> {
val plugin = PluginRepository.list().first { it.type == type }.build(meta)
if (type.isInstance(plugin)) {
@Suppress("UNCHECKED_CAST")
load(plugin as T)
} else {
error("Corrupt type information in plugin repository")
}
}
loaded.meta == meta -> loaded // if meta is the same, return existing plugin
else -> throw RuntimeException("Can't load plugin with type $type. Plugin with this type and different configuration already exists in context.")
}
}
inline fun <reified T : Plugin> load(noinline metaBuilder: MetaBuilder.() -> Unit = {}): T {
return load(T::class, buildMeta(metaBuilder))
}
fun load(name: String, meta: Meta = EmptyMeta): Plugin {
return load(PluginTag.fromString(name), meta)
}
override fun iterator(): Iterator<Plugin> = plugins.iterator()
}

View File

@ -0,0 +1,25 @@
package hep.dataforge.context
import hep.dataforge.meta.Meta
import kotlin.reflect.KClass
interface PluginFactory {
val tag: PluginTag
val type: KClass<Plugin>
fun build(meta: Meta): Plugin
}
object PluginRepository {
/**
* List plugins available in the repository
*/
fun list(): Sequence<PluginFactory> = Global.services(PluginFactory::class)
/**
* Fetch specific plugin and instantiate it with given meta
*/
fun fetch(tag: PluginTag, meta: Meta): Plugin = PluginRepository.list().find { it.tag.matches(tag) }?.build(meta)
?: error("Plugin with tag $tag not found in the repository")
}

View File

@ -0,0 +1,62 @@
package hep.dataforge.context
import hep.dataforge.meta.Meta
import hep.dataforge.meta.MetaRepr
import hep.dataforge.meta.buildMeta
/**
* The tag which contains information about name, group and version of some
* object. It also could contain any complex rule to define version ranges
*
* @author Alexander Nozik
*/
data class PluginTag(
val name: String,
val group: String = "",
val version: String = ""
) : MetaRepr {
/**
* Check if given tag is compatible (in range) of this tag
*
* @param otherTag
* @return
*/
fun matches(otherTag: PluginTag): Boolean {
return matchesName(otherTag) && matchesGroup(otherTag)
}
private fun matchesGroup(otherTag: PluginTag): Boolean {
return this.group.isEmpty() || this.group == otherTag.group
}
private fun matchesName(otherTag: PluginTag): Boolean {
return this.name == otherTag.name
}
override fun toString(): String = listOf(group, name, version).joinToString(separator = ":")
override fun toMeta(): Meta = buildMeta {
"name" to name
"group" to group
"version" to version
}
companion object {
/**
* Build new PluginTag from standard string representation
*
* @param tag
* @return
*/
fun fromString(tag: String): PluginTag {
val sepIndex = tag.indexOf(":")
return if (sepIndex >= 0) {
PluginTag(group = tag.substring(0, sepIndex), name = tag.substring(sepIndex + 1))
} else {
PluginTag(tag)
}
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2015 Alexander Nozik.
*
* 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 hep.dataforge.provider
import hep.dataforge.names.Name
import hep.dataforge.names.toName
/**
*
*
* Path interface.
*
* @author Alexander Nozik
* @version $Id: $Id
*/
inline class Path(val tokens: List<PathToken>) : Iterable<PathToken> {
val head: PathToken? get() = tokens.firstOrNull()
val length: Int get() = tokens.size
/**
* Returns non-empty optional containing the chain without first segment in case of chain path.
*
* @return
*/
val tail: Path? get() = if (tokens.isEmpty()) null else Path(tokens.drop(1))
override fun iterator(): Iterator<PathToken> = tokens.iterator()
companion object {
const val PATH_SEGMENT_SEPARATOR = "/"
fun parse(path: String): Path {
val head = path.substringBefore(PATH_SEGMENT_SEPARATOR)
val tail = path.substringAfter(PATH_SEGMENT_SEPARATOR)
return PathToken.parse(head).toPath() + parse(tail)
}
}
}
operator fun Path.plus(path: Path) = Path(this.tokens + path.tokens)
data class PathToken(val name: Name, val target: String? = null) {
override fun toString(): String = if (target == null) {
name.toString()
} else {
"$target$TARGET_SEPARATOR$name"
}
companion object {
const val TARGET_SEPARATOR = "::"
fun parse(token: String): PathToken {
val target = token.substringBefore(TARGET_SEPARATOR, "")
val name = token.substringAfter(TARGET_SEPARATOR).toName()
if (target.contains("[")) TODO("target separators in queries are not supported")
return PathToken(name, target)
}
}
}
fun PathToken.toPath() = Path(listOf(this))

View File

@ -0,0 +1,89 @@
/*
* Copyright 2015 Alexander Nozik.
*
* 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 hep.dataforge.provider
import hep.dataforge.names.Name
import hep.dataforge.names.toName
/**
* A marker utility interface for providers.
*
* @author Alexander Nozik
*/
interface Provider {
/**
* Default target for this provider
*
* @return
*/
val defaultTarget: String get() = ""
/**
* Default target for next chain segment
*
* @return
*/
val defaultChainTarget: String get() = ""
/**
* Provide a top level element for this [Provider] or return null if element is not present
*/
fun provideTop(target: String, name: Name): Any?
/**
* [Sequence] of available names with given target. Only top level names are listed, no chain path.
*
* @param target
* @return
*/
fun listTop(target: String): Sequence<Name>
}
fun Provider.provide(path: Path, targetOverride: String? = null): Any? {
if (path.length == 0) throw IllegalArgumentException("Can't provide by empty path")
val first = path.first()
val top = provideTop(targetOverride ?: first.target ?: defaultTarget, first.name)
return when (path.length) {
1 -> top
else -> {
when (top) {
null -> null
is Provider -> top.provide(path.tail!!, targetOverride = defaultChainTarget)
else -> throw IllegalStateException("Chain path not supported: child is not a provider")
}
}
}
}
/**
* Type checked provide
*/
inline fun <reified T : Any> Provider.provide(path: String): T? {
return provide(Path.parse(path)) as? T
}
inline fun <reified T : Any> Provider.provide(target: String, name: String): T? {
return provide(PathToken(name.toName(), target).toPath()) as? T
}
/**
* [Sequence] of all elements with given target
*/
fun Provider.provideAll(target: String): Sequence<Any> {
return listTop(target).map { it -> provideTop(target, it) ?: error("The element $it is declared but not provided") }
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2018 Alexander Nozik.
*
* 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 hep.dataforge.context
import java.util.*
import kotlin.collections.HashMap
private fun Properties.asMeta(): Meta {
return buildMeta {
this@asMeta.forEach { key, value ->
set(key.toString().toName(), value)
}
}
}
/**
* A singleton global context. Automatic root for the whole context hierarchy. Also stores the registry for active contexts.
*
* @author Alexander Nozik
*/
actual object Global : Context, JVMContext("GLOBAL", null, Thread.currentThread().contextClassLoader) {
/**
* Closing all contexts
*
* @throws Exception
*/
@Throws(Exception::class)
override fun close() {
logger.info("Shutting down GLOBAL")
for (ctx in contextRegistry.values) {
ctx.close()
}
super.close()
}
private val contextRegistry = HashMap<String, Context>()
/**
* Get previously builder context o builder a new one
*
* @param name
* @return
*/
@Synchronized
fun getContext(name: String): Context {
return contextRegistry.getOrPut(name) { JVMContext(name) }
}
/**
* Close all contexts and terminate framework
*/
@JvmStatic
fun terminate() {
try {
close()
} catch (e: Exception) {
logger.error("Exception while terminating DataForge framework", e)
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright 2018 Alexander Nozik.
*
* 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 hep.dataforge.context
import mu.KLogger
import mu.KotlinLogging
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.collections.HashSet
import kotlin.reflect.KClass
import kotlin.reflect.full.cast
/**
* The local environment for anything being done in DataForge framework. Contexts are organized into tree structure with [Global] at the top.
* Each context has a set of named [Value] properties which are taken from parent context in case they are not found in local context.
* Context implements [ValueProvider] interface and therefore could be uses as a value source for substitutions etc.
* Context contains [PluginManager] which could be used any number of configurable named plugins.
* @author Alexander Nozik
*/
open class JVMContext(
final override val name: String,
final override val parent: JVMContext? = Global,
classLoader: ClassLoader? = null,
properties: Meta = EmptyMeta
) : Context, AutoCloseable {
private val _properties = Config().apply { update(properties) }
override val properties: Meta
get() = if (parent == null) {
_properties
} else {
Laminate(_properties, parent.properties)
}
override val plugins: PluginManager by lazy { PluginManager(this) }
override val logger: KLogger = KotlinLogging.logger(name)
/**
* A class loader for this context. Parent class loader is used by default
*/
open val classLoader: ClassLoader = classLoader ?: parent?.classLoader ?: Global.classLoader
/**
* A property showing that dispatch thread is started in the context
*/
private var started = false
/**
* A dispatch thread executor for current context
*
* @return
*/
val dispatcher: ExecutorService by lazy {
logger.info("Initializing dispatch thread executor in {}", name)
Executors.newSingleThreadExecutor { r ->
Thread(r).apply {
priority = 8 // slightly higher priority
isDaemon = true
name = this@JVMContext.name + "_dispatch"
}.also { started = true }
}
}
private val serviceCache: MutableMap<Class<*>, ServiceLoader<*>> = HashMap()
override fun <T : Any> services(type: KClass<T>): Sequence<T> {
return serviceCache.getOrPut(type.java) { ServiceLoader.load(type.java, classLoader) }.asSequence().map { type.cast(it) }
}
/**
* Get identity for this context
*
* @return
*/
override fun toMeta(): Meta {
return buildMeta {
"parent" to parent?.name
"properties" to properties.seal()
"plugins" to plugins.map { it.toMeta() }
}
}
/**
* Free up resources associated with this context
*
* @throws Exception
*/
override fun close() {
if (isActive) error("Can't close active context")
//detach all plugins
plugins.forEach { it.detach() }
if (started) {
dispatcher.shutdown()
}
}
private val activators = HashSet<WeakReference<Any>>()
override val isActive: Boolean = activators.all { it.get() == null }
override fun activate(activator: Any) {
activators.add(WeakReference(activator))
}
override fun deactivate(activator: Any) {
activators.removeAll { it.get() == activator }
}
}

View File

@ -0,0 +1,25 @@
plugins {
id 'kotlin-multiplatform'
}
repositories {
jcenter()
}
kotlin {
targets {
fromPreset(presets.jvm, 'jvm')
//fromPreset(presets.js, 'js')
// For ARM, preset should be changed to presets.iosArm32 or presets.iosArm64
// For Linux, preset should be changed to e.g. presets.linuxX64
// For MacOS, preset should be changed to e.g. presets.macosX64
//fromPreset(presets.iosX64, 'ios')
}
sourceSets {
commonMain {
dependencies {
api project(":dataforge-context")
}
}
}
}

25
dataforge-io/build.gradle Normal file
View File

@ -0,0 +1,25 @@
plugins {
id 'kotlin-multiplatform'
}
repositories {
jcenter()
}
kotlin {
targets {
fromPreset(presets.jvm, 'jvm')
//fromPreset(presets.js, 'js')
// For ARM, preset should be changed to presets.iosArm32 or presets.iosArm64
// For Linux, preset should be changed to e.g. presets.linuxX64
// For MacOS, preset should be changed to e.g. presets.macosX64
//fromPreset(presets.iosX64, 'ios')
}
sourceSets {
commonMain {
dependencies {
api project(":dataforge-context")
}
}
}
}

View File

@ -1,6 +1,7 @@
package hep.dataforge.meta.io
import hep.dataforge.meta.*
import hep.dataforge.values.*
import kotlinx.io.core.*
/**
@ -20,20 +21,12 @@ fun MetaFormat.stringify(meta: Meta): String {
return builder.build().readText()
}
fun Meta.asString() = JSONMetaFormat.stringify(this)
fun MetaFormat.parse(str: String): Meta{
return read(ByteReadPacket(str.toByteArray()))
}
///**
// * Resolve format by its name. Null if not provided
// */
//expect fun resolveFormat(name: String): MetaFormat?
//
///**
// * Resolve format by its binary key. Null if not provided
// */
//expect fun resolveFormat(key: Short): MetaFormat?
internal expect fun writeJson(meta: Meta, out: Output)
internal expect fun readJson(input: Input, length: Int = -1): Meta
@ -155,7 +148,7 @@ object BinaryMetaFormat : MetaFormat {
(1..length).forEach { _ ->
val name = readString()
val item = readMetaItem()
set(name, item)
setItem(name, item)
}
}
MetaItem.NodeItem(meta)
@ -163,6 +156,5 @@ object BinaryMetaFormat : MetaFormat {
else -> error("Unknown serialization key character: $keyChar")
}
}
}

View File

@ -19,4 +19,18 @@ class MetaFormatTest{
assertEquals(meta,result)
}
@Test
fun testJsonMetaFormat(){
val meta = buildMeta {
"a" to 22
"node" to {
"b" to "DDD"
"c" to 11.1
}
}
val string = JSONMetaFormat.stringify(meta)
val result = JSONMetaFormat.parse(string)
assertEquals(meta,result)
}
}

View File

@ -2,8 +2,8 @@ package hep.dataforge.meta.io
import hep.dataforge.meta.Meta
import hep.dataforge.meta.MetaItem
import hep.dataforge.meta.Value
import hep.dataforge.names.NameToken
import hep.dataforge.values.Value
/**
* Represent any js object as meta
@ -16,7 +16,7 @@ class JSMeta(val obj: Any) : Meta {
private fun isList(obj: Any): Boolean = js("Array").isArray(obj) as Boolean
private fun isPrimitive(obj: Any?): Boolean = js("obj !== Object(obj)") as Boolean
private fun isPrimitive(@Suppress("UNUSED_PARAMETER") obj: Any?): Boolean = js("obj !== Object(obj)") as Boolean
private fun convert(obj: Any?): MetaItem<out Meta> {
return when (obj) {

View File

@ -5,10 +5,12 @@ import com.github.cliftonlabs.json_simple.JsonObject
import com.github.cliftonlabs.json_simple.Jsoner
import hep.dataforge.meta.*
import hep.dataforge.names.toName
import hep.dataforge.values.*
import kotlinx.io.core.*
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.io.Reader
import java.nio.ByteBuffer
import java.text.ParseException
internal actual fun writeJson(meta: Meta, out: Output) {
@ -31,7 +33,7 @@ private fun Value.toJson(): Any {
}
}
private fun Meta.toJson(): JsonObject {
fun Meta.toJson(): JsonObject {
val builder = JsonObject()
items.forEach { name, item ->
when (item) {
@ -60,9 +62,11 @@ internal actual fun readJson(input: Input, length: Int): Meta {
}
override fun read(cbuf: CharArray, off: Int, len: Int): Int {
val block = input.readText(Charsets.UTF_8, len).toCharArray()
System.arraycopy(block, 0, cbuf, off, block.size)
return block.size
val buffer = ByteBuffer.allocate(len)
val res = input.readAvailable(buffer)
val chars = String(buffer.array()).toCharArray()
System.arraycopy(chars, 0, cbuf, off, chars.size)
return res
}
}
@ -103,13 +107,13 @@ private fun MetaBuilder.appendValue(key: String, value: Any?) {
this[key] = value.toListValue()
} else {
val list = value.map<Any, Meta> {
when(it){
when (it) {
is JsonObject -> it.toMeta()
is JsonArray -> it.toListValue().toMeta()
else -> Value.of(it).toMeta()
}
}
setIndexed(key.toName(),list)
setIndexed(key.toName(), list)
}
}
is Number -> this[key] = NumberValue(value)

View File

@ -1,6 +1,7 @@
package hep.dataforge.meta
import hep.dataforge.names.Name
import hep.dataforge.names.toName
//TODO add validator to configuration
@ -20,7 +21,7 @@ open class Config : MutableMetaNode<Config>() {
fun Meta.toConfig(): Config = this as? Config ?: Config().also { builder ->
this.items.mapValues { entry ->
val item = entry.value
builder[entry.key] = when (item) {
builder[entry.key.toName()] = when (item) {
is MetaItem.ValueItem -> MetaItem.ValueItem(item.value)
is MetaItem.NodeItem -> MetaItem.NodeItem(item.node.toConfig())
}

View File

@ -1,5 +1,7 @@
package hep.dataforge.meta
import hep.dataforge.values.Null
import hep.dataforge.values.Value
import kotlin.jvm.JvmName
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty

View File

@ -7,7 +7,17 @@ import hep.dataforge.names.NameToken
*
*
*/
class Laminate(val layers: List<Meta>) : Meta {
class Laminate(layers: List<Meta>) : Meta {
val layers: List<Meta> = layers.flatMap {
if(it is Laminate){
it.layers
} else{
listOf(it)
}
}
constructor(vararg layers: Meta): this(layers.asList())
override val items: Map<NameToken, MetaItem<out Meta>>
get() = layers.map { it.items.keys }.flatten().associateWith { key ->

View File

@ -5,7 +5,11 @@ import hep.dataforge.meta.MetaItem.NodeItem
import hep.dataforge.meta.MetaItem.ValueItem
import hep.dataforge.names.Name
import hep.dataforge.names.NameToken
import hep.dataforge.names.plus
import hep.dataforge.names.toName
import hep.dataforge.values.Value
import hep.dataforge.values.boolean
/**
* A member of the meta tree. Could be represented as one of following:
@ -17,6 +21,14 @@ sealed class MetaItem<M : Meta> {
data class NodeItem<M : Meta>(val node: M) : MetaItem<M>()
}
/**
* The object that could be represented as [Meta]. Meta provided by [toMeta] method should fully represent object state.
* Meaning that two states with the same meta are equal.
*/
interface MetaRepr {
fun toMeta(): Meta
}
/**
* Generic meta tree representation. Elements are [MetaItem] objects that could be represented by three different entities:
* * [MetaItem.ValueItem] (leaf)
@ -24,9 +36,11 @@ sealed class MetaItem<M : Meta> {
*
* * Same name siblings are supported via elements with the same [Name] but different queries
*/
interface Meta {
interface Meta : MetaRepr {
val items: Map<NameToken, MetaItem<out Meta>>
override fun toMeta(): Meta = this
companion object {
/**
* A key for single value node
@ -58,17 +72,32 @@ operator fun Meta.get(key: String): MetaItem<out Meta>? = get(key.toName())
/**
* Get all items matching given name.
*/
fun Meta.getByName(name: Name): Map<String, MetaItem<out Meta>> {
fun Meta.getAll(name: Name): Map<String, MetaItem<out Meta>> {
if (name.length == 0) error("Can't use empty name for that")
val (body, query) = name.last()!!
val regex = query.toRegex()
return (this[name.cutLast()] as? NodeItem<*>)?.node?.items
?.filter { it.key.body == body && (query.isEmpty()|| regex.matches(it.key.query)) }
?.filter { it.key.body == body && (query.isEmpty() || regex.matches(it.key.query)) }
?.mapKeys { it.key.query }
?: emptyMap()
}
/**
* Transform meta to sequence of [Name]-[Value] pairs
*/
fun Meta.asValueSequence(): Sequence<Pair<Name, Value>> {
return items.asSequence().flatMap { entry ->
val item = entry.value
when (item) {
is ValueItem -> sequenceOf(entry.key.toName() to item.value)
is NodeItem -> item.node.asValueSequence().map { pair -> (entry.key.toName() + pair.first) to pair.second }
}
}
}
operator fun Meta.iterator(): Iterator<Pair<Name, Value>> = asValueSequence().iterator()
/**
* A meta node that ensures that all of its descendants has at least the same type
*/

View File

@ -1,6 +1,8 @@
package hep.dataforge.meta
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.values.Value
/**
* DSL builder for meta. Is not intended to store mutable state
@ -36,7 +38,7 @@ fun Meta.builder(): MetaBuilder {
return MetaBuilder().also { builder ->
items.mapValues { entry ->
val item = entry.value
builder[entry.key] = when (item) {
builder[entry.key.toName()] = when (item) {
is MetaItem.ValueItem -> MetaItem.ValueItem<MetaBuilder>(item.value)
is MetaItem.NodeItem -> MetaItem.NodeItem(item.node.builder())
}

View File

@ -4,6 +4,7 @@ import hep.dataforge.names.Name
import hep.dataforge.names.NameToken
import hep.dataforge.names.plus
import hep.dataforge.names.toName
import hep.dataforge.values.Value
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)
@ -77,7 +78,7 @@ abstract class MutableMetaNode<M : MutableMetaNode<M>> : MetaNode<M>(), MutableM
override operator fun set(name: Name, item: MetaItem<M>?) {
when (name.length) {
0 -> error("Can't set meta item for empty name")
0 -> error("Can't setValue meta item for empty name")
1 -> {
val token = name.first()!!
replaceItem(token, get(name), item)
@ -98,24 +99,27 @@ abstract class MutableMetaNode<M : MutableMetaNode<M>> : MetaNode<M>(), MutableM
fun <M : MutableMeta<M>> M.remove(name: Name) = set(name, null)
fun <M : MutableMeta<M>> M.remove(name: String) = remove(name.toName())
operator fun <M : MutableMeta<M>> M.set(name: Name, value: Value) = set(name, MetaItem.ValueItem(value))
operator fun <M : MutableMetaNode<M>> M.set(name: Name, meta: Meta) = set(name, MetaItem.NodeItem(wrap(name, meta)))
operator fun <M : MutableMeta<M>> M.set(name: String, item: MetaItem<M>) = set(name.toName(), item)
operator fun <M : MutableMeta<M>> M.set(name: String, value: Value) = set(name.toName(), MetaItem.ValueItem(value))
operator fun <M : MutableMetaNode<M>> M.set(name: String, meta: Meta) = set(name.toName(), meta)
operator fun <M : MutableMeta<M>> M.set(token: NameToken, item: MetaItem<M>?) = set(token.toName(), item)
fun <M : MutableMeta<M>> M.setValue(name: Name, value: Value) = set(name, MetaItem.ValueItem(value))
fun <M : MutableMeta<M>> M.setItem(name: String, item: MetaItem<M>) = set(name.toName(), item)
fun <M : MutableMeta<M>> M.setValue(name: String, value: Value) = set(name.toName(), MetaItem.ValueItem(value))
fun <M : MutableMeta<M>> M.setItem(token: NameToken, item: MetaItem<M>?) = set(token.toName(), item)
fun <M : MutableMetaNode<M>> M.setNode(name: Name, node: Meta) = set(name, MetaItem.NodeItem(wrap(name, node)))
fun <M : MutableMetaNode<M>> M.setNode(name: String, node: Meta) = setNode(name.toName(), node)
/**
* Universal set method
*/
operator fun <M : MutableMeta<M>> M.set(key: String, value: Any?) {
operator fun <M : MutableMetaNode<M>> M.set(name: Name, value: Any?) {
when (value) {
null -> remove(key)
is Meta -> set(key, value)
else -> set(key, Value.of(value))
null -> remove(name)
is Meta -> setNode(name, value)
else -> setValue(name, Value.of(value))
}
}
operator fun <M : MutableMetaNode<M>> M.set(key: String, value: Any?) = set(key.toName(), value)
/**
* Update existing mutable node with another node. The rules are following:
* * value replaces anything
@ -126,9 +130,9 @@ fun <M : MutableMetaNode<M>> M.update(meta: Meta) {
meta.items.forEach { entry ->
val value = entry.value
when (value) {
is MetaItem.ValueItem -> this[entry.key.toName()] = value.value
is MetaItem.ValueItem -> setValue(entry.key.toName(),value.value)
is MetaItem.NodeItem -> (this[entry.key.toName()] as? MetaItem.NodeItem)?.node?.update(value.node)
?: run { this[entry.key.toName()] = value.node }
?: run { setNode(entry.key.toName(),value.node)}
}
}
}

View File

@ -27,8 +27,8 @@ class StyledConfig(val config: Config, style: Meta = EmptyMeta) : Config() {
override fun set(name: Name, item: MetaItem<Config>?) {
when (item) {
null -> config.remove(name)
is MetaItem.ValueItem -> config[name] = item.value
is MetaItem.NodeItem -> config[name] = item.node
is MetaItem.ValueItem -> config.setValue(name, item.value)
is MetaItem.NodeItem -> config.setNode(name, item.node)
}
}

View File

@ -6,7 +6,7 @@ package hep.dataforge.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>) {
inline class Name constructor(val tokens: List<NameToken>) {
val length
get() = tokens.size
@ -35,19 +35,6 @@ class Name internal constructor(val tokens: List<NameToken>) {
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 = "."
}
@ -80,7 +67,7 @@ fun String.toName(): Name {
var bracketCount: Int = 0
fun queryOn() = bracketCount > 0
this@toName.asSequence().forEach {
asSequence().forEach {
if (queryOn()) {
when (it) {
'[' -> bracketCount++

View File

@ -1,4 +1,4 @@
package hep.dataforge.meta
package hep.dataforge.values
/**
@ -44,19 +44,22 @@ interface Value {
get() = listOf(this)
companion object {
const val VALUE_TARGET = "value"
/**
* Convert object to value
*/
fun of(value: Any?): Value {
return when (value) {
null -> Null
is Value -> value
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")
else -> throw IllegalArgumentException("Unrecognized type of the object (${value::class}) converted to Value")
}
}
}
@ -70,6 +73,8 @@ object Null : Value {
override val type: ValueType get() = ValueType.NULL
override val number: Number get() = Double.NaN
override val string: String get() = "@null"
override fun toString(): String = value.toString()
}
/**
@ -86,6 +91,8 @@ object True : Value {
override val type: ValueType get() = ValueType.BOOLEAN
override val number: Number get() = 1.0
override val string: String get() = "+"
override fun toString(): String = value.toString()
}
/**
@ -106,12 +113,21 @@ class NumberValue(override val number: Number) : Value {
override val string: String get() = number.toString()
override fun equals(other: Any?): Boolean {
return this.number == (other as? Value)?.number
if (other !is Value) return false
return when (number) {
is Short -> number == other.number.toShort()
is Long -> number == other.number.toLong()
is Byte -> number == other.number.toByte()
is Int -> number == other.number.toInt()
is Float -> number == other.number.toFloat()
is Double -> number == other.number.toDouble()
else -> number.toString() == other.number.toString()
}
}
override fun hashCode(): Int = number.hashCode()
override fun toString(): String = value.toString()
}
class StringValue(override val string: String) : Value {
@ -123,7 +139,9 @@ class StringValue(override val string: String) : Value {
return this.string == (other as? Value)?.string
}
override fun hashCode(): Int = string.hashCode()
override fun hashCode(): Int = string.hashCode()
override fun toString(): String = value.toString()
}
class EnumValue<E : Enum<*>>(override val value: E) : Value {
@ -136,6 +154,8 @@ class EnumValue<E : Enum<*>>(override val value: E) : Value {
}
override fun hashCode(): Int = value.hashCode()
override fun toString(): String = value.toString()
}
class ListValue(override val list: List<Value>) : Value {
@ -149,6 +169,8 @@ class ListValue(override val list: List<Value>) : Value {
override val type: ValueType get() = list.first().type
override val number: Number get() = list.first().number
override val string: String get() = list.first().string
override fun toString(): String = value.toString()
}
/**
@ -201,8 +223,8 @@ fun String.parseValue(): Value {
return StringValue(this)
}
class LazyParsedValue(override val string: String): Value{
private val parsedValue by lazy { string.parseValue() }
class LazyParsedValue(override val string: String) : Value {
private val parsedValue by lazy { string.parseValue() }
override val value: Any?
get() = parsedValue.value
@ -210,4 +232,6 @@ class LazyParsedValue(override val string: String): Value{
get() = parsedValue.type
override val number: Number
get() = parsedValue.number
override fun toString(): String = value.toString()
}

View File

@ -1,5 +1,6 @@
package hep.dataforge.meta
import hep.dataforge.values.asValue
import kotlin.test.Test
import kotlin.test.assertEquals

View File

@ -0,0 +1,33 @@
package hep.dataforge.meta
import hep.dataforge.values.NumberValue
import hep.dataforge.values.True
import hep.dataforge.values.Value
import kotlin.test.Test
import kotlin.test.assertEquals
class MetaTest {
@Test
fun valueEqualityTest() {
assertEquals(NumberValue(22), NumberValue(22))
assertEquals(NumberValue(22.0), NumberValue(22))
assertEquals(True, Value.of(true))
}
@Test
fun metaEqualityTest() {
val meta1 = buildMeta {
"a" to 22
"b" to {
"c" to "ddd"
}
}
val meta2 = buildMeta {
"b" to {
"c" to "ddd"
}
"a" to 22
}.seal()
assertEquals<Meta>(meta1, meta2)
}
}

View File

@ -9,4 +9,11 @@ class NameTest{
val name = "token1.token2.token3".toName()
assertEquals("token2", name[1].toString())
}
@Test
fun equalityTest(){
val name1 = "token1.token2[2].token3".toName()
val name2 = "token1".toName() + "token2[2].token3"
assertEquals(name1,name2)
}
}

View File

@ -0,0 +1,25 @@
plugins {
id 'kotlin-multiplatform'
}
repositories {
jcenter()
}
kotlin {
targets {
fromPreset(presets.jvm, 'jvm')
//fromPreset(presets.js, 'js')
// For ARM, preset should be changed to presets.iosArm32 or presets.iosArm64
// For Linux, preset should be changed to e.g. presets.linuxX64
// For MacOS, preset should be changed to e.g. presets.macosX64
//fromPreset(presets.iosX64, 'ios')
}
sourceSets {
commonMain {
dependencies {
api project(":dataforge-context")
}
}
}
}

View File

@ -0,0 +1,25 @@
plugins {
id 'kotlin-multiplatform'
}
repositories {
jcenter()
}
kotlin {
targets {
fromPreset(presets.jvm, 'jvm')
//fromPreset(presets.js, 'js')
// For ARM, preset should be changed to presets.iosArm32 or presets.iosArm64
// For Linux, preset should be changed to e.g. presets.linuxX64
// For MacOS, preset should be changed to e.g. presets.macosX64
//fromPreset(presets.iosX64, 'ios')
}
sourceSets {
commonMain {
dependencies {
api project(":dataforge-context")
}
}
}
}

View File

@ -27,4 +27,12 @@ rootProject.name = 'dataforge-core'
include ":dataforge-meta"
include ":dataforge-meta-io"
include ":dataforge-context"
include ":dataforge-data"
include ":dataforge-io"
include ":dataforge-tables"
include ":dataforge-workspace"
//include ":dataforge-envelope"