All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.zepben.evolve.services.common.BaseService.kt Maven / Gradle / Ivy

There is a newer version: 0.24.0rc1
Show newest version
/*
 * Copyright 2020 Zeppelin Bend Pty Ltd
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

package com.zepben.evolve.services.common

import com.zepben.evolve.cim.iec61970.base.core.IdentifiedObject
import com.zepben.evolve.cim.iec61970.base.core.NameType
import com.zepben.evolve.services.common.exceptions.UnsupportedIdentifiedObjectException
import com.zepben.evolve.services.common.extensions.asUnmodifiable
import com.zepben.evolve.services.common.extensions.nameAndMRID
import com.zepben.evolve.services.common.extensions.typeNameAndMRID
import java.util.*
import java.util.function.Predicate
import java.util.stream.Stream
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KVisibility
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.isSuperclassOf
import kotlin.streams.asStream

/**
 * Base class for services that work with [IdentifiedObject]s. This allows multiple services to be implemented that work
 * with different subsets of the CIM to allow separation of concerns.
 *
 * This class provides a common set of functionality that all services will need.
 *
 * @property [name] a short description of the service
 */
abstract class BaseService(
    val name: String
) {
    protected val objectsByType: MutableMap, MutableMap> = mutableMapOf()

    /**
     * A map of references between mRID's that as yet have not been resolved - typically when transferring services between systems.
     * The key is the toMrid of the [UnresolvedReference]s, and the value is a list of [UnresolvedReference]s for that specific object.
     * For example, if an AcLineSegment with mRID 'acls1' is present in the service, but the service is missing its 'location' with mRID 'location-l1'
     * and 'perLengthSequenceImpedance' with mRID 'plsi-1', the following key value pairs would be present:
     * {
     *   "plsi-1": [
     *     UnresolvedReference(from=AcLineSegment('acls1'), toMrid='plsi-1', resolver=ReferenceResolver(fromClass=AcLineSegment, toClass=PerLengthSequenceImpedance, resolve=...), ...)
     *   ],
     *   "location-l1": [
     *     UnresolvedReference(from=AcLineSegment('acls1'), toMrid='location-l1', resolver=ReferenceResolver(fromClass=AcLineSegment, toClass=Location, resolve=...), ...)
     *   ]
     * }
     *
     * [resolve] in [ReferenceResolver] will be the function used to populate the relationship between the [IdentifiedObject]s either when
     * [resolveOrDeferReference] is called if the other side of the reference exists in the service, or otherwise when the second object is added to the service.
     */
    private val unresolvedReferencesTo = mutableMapOf>>()

    /**
     * An index of the unresolved references by their [UnresolvedReference.from] mRID. For the above example this will be a dictionary of the form:
     * {
     *   "acls1": [
     *     UnresolvedReference(from=AcLineSegment('acls1'), toMrid='location-l1', resolver=ReferenceResolver(fromClass=AcLineSegment, toClass=Location, resolve=...), ...)
     *     UnresolvedReference(from=AcLineSegment('acls1'), toMrid='plsi-1', resolver=ReferenceResolver(fromClass=AcLineSegment, toClass=PerLengthSequenceImpedance, resolve=...), ...)
     *   ]
     * }
     */
    private val unresolvedReferencesFrom = mutableMapOf>>()

    private val addFunctions: Map, KFunction<*>> = findFunctionsForDispatch("add")
    private val removeFunctions: Map, KFunction<*>> = findFunctionsForDispatch("remove")

    val supportedClasses: Set> = Collections.unmodifiableSet(addFunctions.keys.map { it.java }.toSet())
    val supportedKClasses: Set> get() = addFunctions.keys

    private var _nameTypes: MutableMap = mutableMapOf()

    init {
        check(addFunctions.keys == removeFunctions.keys) {
            "Add and remove functions should be defined in matching pairs. They don't seem to match...\n" +
                "add   : ${addFunctions.keys.sortedBy { it.simpleName }}\n" +
                "remove: ${removeFunctions.keys.sortedBy { it.simpleName }}"
        }
    }

    /**
     * Get an object associated with this service.
     *
     * @param T The type of object to look for. If this is a base class it will search all subclasses.
     * @param mRID The mRID of the object to find.
     *
     * @return The object identified by [mRID] as [T] if it was found, otherwise null.
     */
    inline operator fun  get(mRID: String?): T? {
        return get(T::class, mRID)
    }

    /**
     * A Java interop version of [get]. Get an object associated with this service.
     *
     * @param T The type of object to look for. If this is a base class it will search all subclasses.
     * @param clazz The class representing [T].
     * @param mRID The mRID of the object to find.
     *
     * @return The object identified by [mRID] as [T] if it was found, otherwise null.
     */
    fun  get(clazz: Class, mRID: String?): T? {
        return get(clazz.kotlin, mRID)
    }

    /**
     * The name types associated with this service. The returned collection is read only.
     */
    val nameTypes: Collection get() = _nameTypes.values.asUnmodifiable()

    /**
     * Associates the provided [nameType] with this service.
     *
     * @param [nameType] the [NameType] to add to this service
     * @return true if the object is associated with this service, false if an object already exists in the service with
     * the same name.
     */
    fun addNameType(nameType: NameType): Boolean {
        if (_nameTypes.containsKey(nameType.name)) {
            return false
        }
        _nameTypes[nameType.name] = nameType

        return true
    }

    /**
     * Gets the [NameType] for the provided type name associated with this service.
     *
     * @param [type] the type name.
     * @return The [NameType] identified by [type] if it was found, otherwise null.
     */
    fun getNameType(type: String): NameType? = _nameTypes[type]

    /**
     * Get an object associated with this service. If the object exists with this service but is not an instance of [T]
     * a [ClassCastException] is thrown.
     *
     * @param T The type of object to look for. If this is a base class it will search all subclasses.
     * @param clazz The class representing [T].
     * @param mRID The mRID of the object to find.
     * @return The object identified by [mRID] as [T] if it was found, otherwise null.
     */
    @Suppress("UNCHECKED_CAST")
    fun  get(clazz: KClass, mRID: String?): T? {
        mRID ?: return null

        if (clazz != IdentifiedObject::class)
            objectsByType[clazz]?.let { return it[mRID] as T? }

        return clazz.java.cast(objectsByType.values
            .asSequence()
            .mapNotNull { it[mRID] }
            .firstOrNull()
        )
    }

    /**
     * Check if [mRID] has any associated object.
     *
     * @param mRID The mRID to search for.
     *
     * @return true if there is an object associated with the specified [mRID].
     */
    fun contains(mRID: String): Boolean = objectsByType.values.any { it.containsKey(mRID) }

    /**
     * Get the number of objects associated with this service.
     *
     * @param T The type of object to count. If this is a base class it will search all subclasses.
     *
     * @return The number of objects of the specified type.
     */
    inline fun  num(): Int {
        return num(T::class)
    }

    /**
     * A Java interop version of [num]. Get the number of objects associated with this service.
     *
     * @param T The type of object to count. If this is a base class it will search all subclasses.
     * @param clazz The class representing [T].
     *
     * @return The number of objects of the specified type.
     */
    fun  num(clazz: Class): Int {
        return num(clazz.kotlin)
    }

    /**
     * Get the number of objects associated with this service.
     *
     * @param T The type of object to count. If this is a base class it will search all subclasses.
     * @param clazz The class representing [T].
     *
     * @return The number of objects of the specified type.
     */
    fun  num(clazz: KClass): Int {
        return sequenceOf(clazz).count()
    }

    /**
     * Attempts to add the [identifiedObject] to the service, if this service instance supports the type of [IdentifiedObject]
     * that is provided.
     *
     * If the service does support the [identifiedObject], it will be added as if you are calling the add function
     * directly on the instance where the corresponding "add" function is defined for this type of identified object.
     *
     * @throws [UnsupportedIdentifiedObjectException] if the service does not support the [identifiedObject].
     * @return the return value of the underlying add function.
     */
    fun tryAdd(identifiedObject: IdentifiedObject): Boolean {
        val func = addFunctions[identifiedObject::class]
            ?: throw UnsupportedIdentifiedObjectException("$name service does not support adding ${identifiedObject::class}")

        return func.call(this, identifiedObject) as Boolean
    }

    /**
     * Add to the service and return [cim] if successful or null if the add failed (typically due to mRID already existing)
     * @param cim The [IdentifiedObject] to add.
     *
     * @return [cim] if successfully added else null.
     */
    fun  tryAddOrNull(cim: T): T? =
        try {
            if (tryAdd(cim))
                cim
            else
                null
        } catch (ex: UnsupportedIdentifiedObjectException) {
            null
        }


    /**
     * Attempts to remove the [identifiedObject] to the service, if this service instance supports the type of [IdentifiedObject]
     * that is provided.
     *
     * If the service does support the [identifiedObject], it will be removed as if you are calling the remove function
     * directly on the instance where the corresponding remove function is defined for this type of identified object.
     *
     * @throws [UnsupportedIdentifiedObjectException] if the service does not support the [identifiedObject].
     * @return the return value of the underlying remove function.
     */
    fun tryRemove(identifiedObject: IdentifiedObject): Boolean {
        val func = removeFunctions[identifiedObject::class]
            ?: throw UnsupportedIdentifiedObjectException("$name service does not support removing ${identifiedObject::class}")

        return func.call(this, identifiedObject) as Boolean
    }

    /**
     * Associates the provided [identifiedObject] with this service. This should be called by derived classes within their
     * add functions for specific supported identified object types.
     *
     * The [identifiedObject] must have a unique MRID, otherwise false will be returned and the object will not be added.
     *
     * If there are any unresolved references to the [identifiedObject] at this point they will be resolved
     * as part of the addition. If the [identifiedObject] class type does not match the [ReferenceResolver.toClass] of
     * any unresolved references an [IllegalStateException] will be thrown.
     *
     * @param [identifiedObject] the object to add to this service
     * @throws [UnsupportedIdentifiedObjectException] if the [IdentifiedObject] is not supported by this service.
     * @throws [IllegalStateException] if any unresolved references have an incorrect class type.
     * @throws [IllegalArgumentException] if [identifiedObject] did not have a valid mRID.
     * @return true if the object is associated with this service, false if an object already exists in the service with
     * the same mRID.
     */
    protected fun add(identifiedObject: IdentifiedObject): Boolean {
        if (identifiedObject.mRID.isEmpty())
            throw IllegalArgumentException("Object [${identifiedObject.typeNameAndMRID()}] must have an mRID set to be added to the service.")

        if (!supportedKClasses.contains(identifiedObject::class)) {
            throw UnsupportedIdentifiedObjectException("Unsupported IdentifiedObject type: ${identifiedObject::class}")
        }

        val map = objectsByType.computeIfAbsent(identifiedObject::class) { mutableMapOf() }
        if (map.containsKey(identifiedObject.mRID)) return map[identifiedObject.mRID] === identifiedObject

        // Check all the other types to make sure this MRID is actually unique
        if (objectsByType.any { (_, v) -> v.containsKey(identifiedObject.mRID) })
            return false

        unresolvedReferencesTo.remove(identifiedObject.mRID)?.forEach {
            try {
                val castedIdentifiedObject = it.resolver.toClass.cast(identifiedObject)

                it.resolver.resolve(it.from, castedIdentifiedObject)
                it.reverseResolver?.resolve(castedIdentifiedObject, it.from)

                unresolvedReferencesFrom[it.from.mRID]?.let { urs ->
                    urs.remove(it)
                    if (urs.isEmpty())
                        unresolvedReferencesFrom.remove(it.from.mRID)
                }
            } catch (ex: ClassCastException) {
                throw IllegalStateException(
                    "Expected a ${it.resolver.toClass.simpleName} when resolving ${identifiedObject.nameAndMRID()} references but got a ${identifiedObject::class.simpleName}. Make sure you sent the correct types in every reference.",
                    ex
                )
            }
        }

        map[identifiedObject.mRID] = identifiedObject
        return true
    }

    /**
     * Resolves a property reference between a [T] and a referenced [R] by looking up the [toMrid] in the service and
     * using the provided [boundResolver] to resolve the reference relationship for the [T] object within the [BoundReferenceResolver].
     *
     * If the [toMrid] object has not yet been added to the service, the reference resolution will be deferred until the
     * object with [toMrid] is added to the service, which will then use the resolver from the [boundResolver] at that
     * time to resolve the reference relationship.
     *
     * The [toMrid] should be the MRID of an object that is a subclass of type [R]. If it is not, an [IllegalStateException]
     * is thrown either immediately if the reference can be resolved now, or it will be thrown when the deferred resolution
     * is applied when the object is added to the service.
     *
     * @return true if the reference was resolved, otherwise false if it has been deferred.
     */
    @Suppress("UNCHECKED_CAST")
    fun  resolveOrDeferReference(
        boundResolver: BoundReferenceResolver,
        toMrid: String?
    ): Boolean {
        if (toMrid.isNullOrEmpty()) {
            return true
        }

        val (from, resolver, reverseResolver) = boundResolver
        try {
            val to = get(resolver.toClass, toMrid)
            return if (to != null) {
                resolver.resolve(from, to)
                if (reverseResolver != null) {
                    reverseResolver.resolve(to, from)

                    // Clean up any reverse unresolved references now that the reference has been resolved
                    unresolvedReferencesTo[from.mRID]?.apply {
                        removeIf { it.toMrid == from.mRID && it.resolver == reverseResolver }
                        if (isEmpty())
                            unresolvedReferencesTo.remove(from.mRID)
                    }
                    unresolvedReferencesFrom[to.mRID]?.apply {
                        removeIf { it.toMrid == from.mRID && it.resolver == reverseResolver }
                        if (isEmpty())
                            unresolvedReferencesFrom.remove(to.mRID)
                    }
                }
                true
            } else {
                val ur = UnresolvedReference(from, toMrid, resolver, reverseResolver) as UnresolvedReference
                unresolvedReferencesTo.getOrPut(toMrid) { mutableSetOf() }.add(ur)
                unresolvedReferencesFrom.getOrPut(from.mRID) { mutableSetOf() }.add(ur)
                false
            }
        } catch (ex: ClassCastException) {
            throw IllegalStateException(
                "$toMrid didn't match the expected class ${resolver.toClass.simpleName}. Did you re-use an mRID?: ${ex.localizedMessage}",
                ex
            )
        }
    }

    /**
     * Check if there are [UnresolvedReference]s in the service
     *
     * @param mRID The mRID to check for [UnresolvedReference]s. If null, will check if any unresolved references exist in the service.
     *
     * @return true if at least one reference exists.
     */
    fun hasUnresolvedReferences(mRID: String? = null): Boolean = if (mRID != null) unresolvedReferencesTo.containsKey(mRID) else unresolvedReferencesTo.isNotEmpty()

    /**
     * Get the number of [UnresolvedReference]s in this service.
     *
     * @param mRID The mRID to check the number of [UnresolvedReference]s for. If null, will default to number of all unresolved references in the service.
     *
     * @return The number of [UnresolvedReference]s
     */
    fun numUnresolvedReferences(mRID: String? = null): Int =
        mRID?.let { unresolvedReferencesTo[mRID]?.size ?: 0 } ?: unresolvedReferencesTo.values.sumOf { it.size }

    /**
     *
     * Gets a set of MRIDs that are unresolved references via the [referenceResolver].
     */
    fun  getUnresolvedReferenceMrids(referenceResolver: ReferenceResolver): Set =
        unresolvedReferencesTo.values.asSequence()
            .flatMap { it.asSequence() }
            .filter { it.resolver == referenceResolver }
            .map { it.toMrid }
            .toSet()

    /**
     * Gets a set of MRIDs that are referenced by the [T] held by [boundResolver] that are unresolved.
     */
    fun  getUnresolvedReferenceMrids(boundResolver: BoundReferenceResolver): Set =
        unresolvedReferencesTo.values.asSequence()
            .flatMap { it.asSequence() }
            .filter { it.from == boundResolver.from && it.resolver == boundResolver.resolver }
            .map { it.toMrid }
            .toSet()

    /**
     * Get the [UnresolvedReference]s that [mRID] has to other objects.
     * @param mRID The mRID to get unresolved references for.
     * @return a sequence of the [UnresolvedReference]s that need to be resolved for [mRID].
     */
    fun getUnresolvedReferencesFrom(mRID: String): Sequence> = unresolvedReferencesFrom[mRID]?.asSequence() ?: emptySequence()

    /**
     * Get the [UnresolvedReference]s that other objects have to [mRID].
     * @param mRID The mRID to get unresolved references for.
     * @return a sequence of the [UnresolvedReference]s that need to be resolved for [mRID].
     */
    fun getUnresolvedReferencesTo(mRID: String): Sequence> = unresolvedReferencesTo[mRID]?.asSequence() ?: emptySequence()

    /**
     * Returns a sequence of all unresolved references.
     */
    fun unresolvedReferences(): Sequence> =
        unresolvedReferencesTo.values.asSequence().flatMap { it.asSequence() }

    /**
     * Disassociate an object from this service.
     *
     * @param identifiedObject The object to disassociate from this service.
     *
     * @return true if the object is disassociated from this service.
     */
    protected fun remove(identifiedObject: IdentifiedObject): Boolean = objectsByType[identifiedObject::class]?.remove(identifiedObject.mRID) != null

    /**
     * Create a sequence of all instances of the specified type.
     *
     * @param T The type of object to add to the sequence. If this is a base class it will collect all subclasses.
     *
     * @return a [Sequence] containing all instances of type [T].
     */
    inline fun  sequenceOf(): Sequence {
        return sequenceOf(T::class)
    }

    /**
     * Create a sequence of all instances of the specified type.
     *
     * @param T The type of object to add to the sequence. If this is a base class it will collect all subclasses.
     * @param clazz The class representing [T].
     *
     * @return a [Sequence] containing all instances of type [T].
     */
    @Suppress("UNCHECKED_CAST")
    fun  sequenceOf(clazz: KClass): Sequence {
        return objectsByType[clazz]?.values?.asSequence()?.map { it as T }
            ?: objectsByType
                .asSequence()
                .filter { (c, _) -> clazz.isSuperclassOf(c) }
                .flatMap { (_, map) -> map.values.asSequence() }
                .map { it as T }
    }

    /**
     * A Java interop version of [sequenceOf]. Create a sequence of all instances of the specified type.
     *
     * @param T The type of object to add to the sequence. If this is a base class it will collect all subclasses.
     * @param clazz The class representing [T].
     *
     * @return a [Stream] containing all instances of type [T].
     */
    fun  streamOf(clazz: Class): Stream {
        return sequenceOf(clazz.kotlin).asStream()
    }

    /**
     * Collect all instances of the specified type that match a [filter] into a [List].
     *
     * @param T The type of object to collect. If this is a base class it will collect all subclasses.
     * @param filter The filter used to include items in the [List].
     *
     * @return a [List] containing all instances of type [T] that match [filter] stored in this service.
     */
    inline fun  listOf(noinline filter: ((T) -> Boolean)? = null): List {
        return listOf(T::class, filter)
    }

    /**
     * A Java interop version of [listOf]. Collect all instances of the specified type that match a [filter] into a [List].
     *
     * @param T The type of object to collect. If this is a base class it will collect all subclasses.
     * @param clazz The class representing [T].
     * @param filter The filter used to include items in the [List].
     *
     * @return a [List] containing all instances of type [T] that match [filter] stored in this service.
     */
    @JvmOverloads
    fun  listOf(clazz: Class, filter: Predicate? = null): List {
        return listOf(clazz.kotlin, filter?.let { it::test })
    }

    /**
     * Collect all instances of the specified type that match a [filter] into a [List].
     *
     * @param T The type of object to collect. If this is a base class it will collect all subclasses.
     * @param clazz The class representing [T].
     * @param filter The filter used to include items in the [List].
     *
     * @return a [List] containing all instances of type [T] that match [filter] stored in this service.
     */
    fun  listOf(clazz: KClass, filter: ((T) -> Boolean)? = null): List {
        val sequence = sequenceOf(clazz)
        return if (filter != null)
            sequence.filter(filter).toList()
        else
            sequence.toList()
    }

    /**
     * Collect all instances of the specified type that match a [filter] into a [Set].
     *
     * @param T The type of object to collect. If this is a base class it will collect all subclasses.
     * @param filter The filter used to include items in the [Set].
     *
     * @return a [Set] containing all instances of type [T] that match [filter] stored in this service.
     */
    inline fun  setOf(noinline filter: ((T) -> Boolean)? = null): Set {
        return setOf(T::class, filter)
    }

    /**
     * A Java interop version of [setOf]. Collect all instances of the specified type that match a [filter] into a [Set].
     *
     * @param T The type of object to collect. If this is a base class it will collect all subclasses.
     * @param clazz The class representing [T].
     * @param filter The filter used to include items in the [Set].
     *
     * @return a [Set] containing all instances of type [T] that match [filter] stored in this service.
     */
    @JvmOverloads
    fun  setOf(clazz: Class, filter: Predicate? = null): Set {
        return setOf(clazz.kotlin, filter?.let { it::test })
    }

    /**
     * Collect all instances of the specified type that match a [filter] into a [Set].
     *
     * @param T The type of object to collect. If this is a base class it will collect all subclasses.
     * @param clazz The class representing [T].
     * @param filter The filter used to include items in the [Set].
     *
     * @return a [Set] containing all instances of type [T] that match [filter] stored in this service.
     */
    fun  setOf(clazz: KClass, filter: ((T) -> Boolean)? = null): Set {
        val sequence = sequenceOf(clazz)
        return if (filter != null)
            sequence.filter(filter).toSet()
        else
            sequence.toSet()
    }

    /**
     * Collect all instances of the specified type that match a [filter] into a [Map], indexed by [IdentifiedObject.mRID]..
     *
     * @param T The type of object to collect. If this is a base class it will collect all subclasses.
     * @param filter The filter used to include items in the [Map].
     *
     * @return a [Map] containing all instances of type [T] that match [filter] stored in this service.
     */
    inline fun  mapOf(noinline filter: ((T) -> Boolean)? = null): Map {
        return mapOf(T::class, filter)
    }

    /**
     * A Java interop version of [mapOf]. Collect all instances of the specified type that match a [filter] into a [Map], indexed by [IdentifiedObject.mRID]..
     *
     * @param T The type of object to collect. If this is a base class it will collect all subclasses.
     * @param clazz The class representing [T].
     * @param filter The filter used to include items in the [Map].
     *
     * @return a [Map] containing all instances of type [T] that match [filter] stored in this service.
     */
    @JvmOverloads
    fun  mapOf(clazz: Class, filter: Predicate? = null): Map {
        return mapOf(clazz.kotlin, filter?.let { it::test })
    }

    /**
     * Collect all instances of the specified type that match a [filter] into a [Map], indexed by [IdentifiedObject.mRID]..
     *
     * @param T The type of object to collect. If this is a base class it will collect all subclasses.
     * @param clazz The class representing [T].
     * @param filter The filter used to include items in the [Map].
     *
     * @return a [Map] containing all instances of type [T] that match [filter] stored in this service.
     */
    fun  mapOf(clazz: KClass, filter: ((T) -> Boolean)? = null): Map {
        val sequence = sequenceOf(clazz)
        return if (filter != null)
            sequence.filter(filter).associateBy { it.mRID }
        else
            sequence.associateBy { it.mRID }
    }

    @Suppress("UNCHECKED_CAST")
    private fun findFunctionsForDispatch(name: String): Map, KFunction<*>> {
        val idObjType = IdentifiedObject::class.createType()
        return this::class.declaredMemberFunctions.asSequence()
            .filter { it.name == name }
            .filter { it.parameters.size == 2 }
            .filter { it.visibility == KVisibility.PUBLIC }
            .filter { it.parameters[1].type.isSubtypeOf(idObjType) }
            .map { (it.parameters[1].type.classifier as KClass) to it }
            .onEach {
                require(it.second.returnType.classifier == Boolean::class) {
                    "return type for '${it.second}' needs to be Boolean"
                }

                require((it.second.parameters[0].type.classifier as KClass<*>).isFinal) {
                    "${it.second} does not accept a leaf class. " +
                        "Only leafs should be used to reduce chances of edge case issues and potential undefined behaviour"
                }
            }
            .toMap()
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy