Skip to content

Commit

Permalink
support for operation parameter domains #16
Browse files Browse the repository at this point in the history
  • Loading branch information
abstratt committed May 31, 2018
1 parent 07c5506 commit ed6452e
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ open class KirraSpringConfiguration {
@Autowired
private lateinit var schemaManagement: SchemaManagement

@PersistenceContext
private lateinit var entityManager : EntityManager

@Bean
open fun instanceManagement() : InstanceManagement =
KirraSpringInstanceManagement(kirraSpringMetamodel, kirraSpringInstanceBridge, schemaManagement, securityService)
KirraSpringInstanceManagement(kirraSpringMetamodel, kirraSpringInstanceBridge, schemaManagement, securityService, entityManager)

@Autowired
private fun createServices(schema : Schema) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,6 @@ class KirraSpringInstanceBridge {
return kirraValue
}

fun toExternalId(id: Long?): String? = id?.toString()

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import java.lang.reflect.Method
import javax.persistence.EntityManager
import javax.persistence.PersistenceContext
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.*
import kotlin.reflect.jvm.javaMethod

@Service
@Transactional
open class KirraSpringInstanceManagement (
private val kirraSpringMetamodel: KirraSpringMetamodel,
private val kirraSpringInstanceBridge: KirraSpringInstanceBridge,
private val schemaManagement: SchemaManagement,
private val securityService: SecurityService
private val securityService: SecurityService,
private val entityManager : EntityManager
) : InstanceManagement {

@Secured
Expand All @@ -44,6 +46,9 @@ open class KirraSpringInstanceManagement (
return kirraSpringInstanceBridge.toInstance(toConvert, InstanceManagement.DataProfile.Full)
}

private fun retrieveJavaInstance(typeRef : TypeRef, externalId: String): BaseEntity? =
retrieveJavaInstance(typeRef.namespace, typeRef.typeName, externalId)

private fun retrieveJavaInstance(namespace: String, entityName: String, externalId: String): BaseEntity? {
val entityClass: Class<BaseEntity>? = kirraSpringMetamodel.getEntityClass(namespace, entityName)
val asService: BaseService<BaseEntity, *> = getEntityService(TypeRef(namespace, entityName, TypeRef.TypeKind.Entity))
Expand Down Expand Up @@ -87,6 +92,7 @@ open class KirraSpringInstanceManagement (
}

override fun saveContext() {
entityManager.flush()
}

override fun getEntityCapabilities(typeRef: TypeRef): EntityCapabilities {
Expand Down Expand Up @@ -237,13 +243,17 @@ open class KirraSpringInstanceManagement (
}
var customImplArguments : Map<String, Any?> = mapOf(Pair("pageRequest", pageRequest))

val (service, javaInstance: Any?, result) = doExecuteOperation(operation, externalId, arguments, customImplArguments)
return result
val result = doExecuteOperation(operation, externalId, arguments, customImplArguments)
return result.second
}

@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
override fun executeOperation(operation: Operation, externalId: String?, arguments: MutableList<*>?): MutableList<*> {
val (service, javaInstance: Any?, result) = doExecuteOperation(operation, externalId, arguments)
val service = getEntityService(operation.owner)

val entityInstance = externalId?.let { service.findById(it.toLong()) }

val (javaInstance, result) = doExecuteOperation(operation, externalId, arguments)

if (javaInstance is BaseEntity) {
service.update(javaInstance)
Expand All @@ -252,38 +262,21 @@ open class KirraSpringInstanceManagement (
return result
}

private fun doExecuteOperation(operation: Operation, externalId: String?, arguments: MutableList<*>?, customImplArguments : Map<String, Any?> = emptyMap()): Triple<BaseService<BaseEntity, *>, Any?, MutableList<*>> {
val kirraEntity = schemaManagement.getEntity(operation.owner)
val entityClass: Class<BaseEntity> = kirraSpringMetamodel.getEntityClass(kirraEntity.entityNamespace, kirraEntity.name)!!
val service = getEntityService(operation.owner)

val entityInstance = externalId?.let { service.findById(it.toLong()) }

val matchedModelArguments = matchArguments(arguments, operation)
val matchedArguments = matchedModelArguments + customImplArguments

// it will be null if the service does not have a function matching the Kirra operation name
val serviceImplClass = AopUtils.getTargetClass(service)
data class MatchedOperation(
val javaInstance : Any, val matchedModelArguments :Map<String, Any?>
)

val serviceImplementationMethod = findMatchingMethod(serviceImplClass.kotlin, operation.name, matchedArguments)
// we need the proxy function though
val serviceMethod = serviceImplementationMethod?.let { impl ->
service::class.java.methods.find {
// same name and parameter types
it.name == operation.name && it.parameterTypes.toList() == impl!!.parameterTypes.toList()
}
}
private fun doExecuteOperation(operation: Operation, externalId: String?, arguments: MutableList<*>?, customImplArguments : Map<String, Any?> = emptyMap()): Pair<Any, MutableList<*>> {
val (entityInstance, matchedModelArguments, pair) = matchOperation(operation, externalId, arguments, customImplArguments)

val entityMethod = findMatchingMethod(entityClass.kotlin, operation.name, matchedModelArguments)
val (javaInstance, actualMethod) = pair

KirraException.ensure(entityMethod != null || serviceImplementationMethod != null, KirraException.Kind.ELEMENT_NOT_FOUND, { "No operation named ${operation.name}" })

val (javaInstance, actualMethod, functionParameterNames) = if (serviceMethod != null) Triple(service, serviceMethod!!, serviceImplementationMethod.parameters.map { it.name }) else Triple(entityInstance, entityMethod!!, entityMethod.parameters.map { it.name })
val isServiceMethod = javaInstance is BaseService<*,*>
BusinessException.ensure(javaInstance != null, ErrorCode.UNKNOWN_OBJECT)

val boundMethod = actualMethod

val offset = if (operation.isInstanceOperation && serviceMethod != null) 1 else 0
val offset = if (operation.isInstanceOperation && isServiceMethod) 1 else 0
var actualParameterIndex = -1
val kotlinMatchedModelArguments = matchedModelArguments.map { namedValue ->
val index = operation.parameters.indexOfFirst { it.name == namedValue.key }
Expand All @@ -300,11 +293,11 @@ open class KirraSpringInstanceManagement (

val kotlinMatchedArguments = (kotlinMatchedModelArguments + kotlinMatchedImplArguments).toMutableList()

if (serviceMethod != null) {
if (isServiceMethod) {
// a service may implement an instance operation - in that case, the entity instance is the first parameter
if (operation.isInstanceOperation) {
// first parameter holds the target instance, by convention
val serviceParameter = serviceMethod.parameters[0]
val serviceParameter = actualMethod.parameters[0]
kotlinMatchedArguments.add(0, Pair(serviceParameter, entityInstance))
}
}
Expand All @@ -321,7 +314,40 @@ open class KirraSpringInstanceManagement (
kirraSpringInstanceBridge.toInstance(toConvert, InstanceManagement.DataProfile.Full)
} else it }.toMutableList()

return Triple(service, javaInstance, result)
return Pair(javaInstance!!, result)
}

private fun matchOperation(operation: Operation, externalId: String?, arguments: MutableList<*>?, customImplArguments: Map<String, Any?>): Triple<BaseEntity?, Map<String, Any?>, Pair<Any?, Method>> {
val kirraEntity = schemaManagement.getEntity(operation.owner)
val entityClass: Class<BaseEntity> = kirraSpringMetamodel.getEntityClass(kirraEntity.entityNamespace, kirraEntity.name)!!
val service = getEntityService(operation.owner)

val entityInstance = externalId?.let { service.findById(it.toLong()) }

val matchedModelArguments = matchArguments(arguments, operation)
val matchedArguments = matchedModelArguments + customImplArguments

// it will be null if the service does not have a function matching the Kirra operation name
val serviceImplClass = AopUtils.getTargetClass(service)

val serviceImplementationMethod = findMatchingMethod(serviceImplClass.kotlin, operation.name, matchedArguments)
// we need the proxy function though
val serviceMethod = serviceImplementationMethod?.let { impl ->
service::class.java.methods.find {
// same name and parameter types
it.name == operation.name && it.parameterTypes.toList() == impl!!.parameterTypes.toList()
}
}

val entityMethod = findMatchingMethod(entityClass.kotlin, operation.name, matchedModelArguments)

KirraException.ensure(entityMethod != null || serviceImplementationMethod != null, KirraException.Kind.ELEMENT_NOT_FOUND, { "No operation named ${operation.name}" })

val pair = if (entityMethod == null)
Pair(service, serviceMethod!!)
else
Pair(entityInstance, entityMethod!!)
return Triple(entityInstance, matchedModelArguments, pair)
}

private fun findMatchingMethod(kClass: KClass<out Any>, operationName: String?, matchedArguments: Map<String, Any?>): Method? {
Expand Down Expand Up @@ -360,8 +386,24 @@ open class KirraSpringInstanceManagement (
}.filter { it.second != null }.toMap()
}

override fun getParameterDomain(entity: Entity, externalId: String, action: Operation, parameter: Parameter): MutableList<Instance> {
//TODO-RC honor parameter constraints
override fun getParameterDomain(entity: Entity, externalId: String?, action: Operation, parameter: Parameter): MutableList<Instance> {
val accessorPair = kirraSpringMetamodel.findDomainAccessor(action, parameter)
if (accessorPair != null) {
val (accessorClass, accessorMethod) = accessorPair
val entityDefinedAccessor = accessorClass.isSubclassOf(BaseEntity::class)
val found = action.isInstanceOperation.ifTrue {
retrieveJavaInstance(entity.typeRef, externalId!!)
}
val pageRequest : PageRequest? = defaultPageRequest()
val entityService = getEntityService(entity.typeRef)
val domain =
when {
entityDefinedAccessor -> accessorMethod.call(found, pageRequest)
action.isInstanceOperation -> accessorMethod.call(entityService, found, pageRequest)
else -> accessorMethod.call(entityService, pageRequest)
}
return kirraSpringInstanceBridge.toInstances(domain.content as Iterable<BaseEntity>).toMutableList()
}
return getInstances(parameter.typeRef.entityNamespace, parameter.typeRef.typeName, InstanceManagement.DataProfile.Slim)
}

Expand Down Expand Up @@ -428,4 +470,3 @@ class BoundFunction<R>(val instance : Any, val baseFunction : KFunction<R>) : KF

}


36 changes: 27 additions & 9 deletions src/main/java/com/abstratt/kirra/spring/KirraSpringMetamodel.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.abstratt.kirra.spring

import com.abstratt.kirra.Operation
import com.abstratt.kirra.Relationship
import com.abstratt.kirra.TypeRef
import com.abstratt.kirra.*
import org.reflections.Reflections
import org.reflections.util.ConfigurationBuilder
import org.springframework.aop.support.AopUtils
import org.springframework.beans.factory.NoSuchBeanDefinitionException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.data.domain.Page
import org.springframework.stereotype.Component
import org.springframework.stereotype.Service
import java.lang.reflect.AccessibleObject
Expand Down Expand Up @@ -81,6 +80,31 @@ class KirraSpringMetamodel {
fun isEntityClass(clazz: Class<*>): Boolean =
entityClasses.containsKey(clazz.name)

fun findDomainAccessor(operation: Operation, parameter: Parameter) : Pair<KClass<*>, KFunction<Page<*>>>? {
return findDomainAccessorInClass(getEntityClass(operation.owner)!!.kotlin, operation, parameter) ?:
findDomainAccessorInClass(getEntityServiceClass<BaseService<*,*>>(operation.owner), operation, parameter)
}

private fun findDomainAccessorInClass(kClass: KClass<*>?, operation: Operation, parameter: Parameter): Pair<KClass<*>, KFunction<Page<*>>>? {
if (kClass == null)
return null
// val kFunction = kClass.functions.find { it.name == operation.name}
// if (kFunction == null)
// return null
// val kParameter = kFunction.valueParameters.find { it.name == parameter.name }
// if (kParameter == null)
// return null
// val domainAnnotation = kParameter.findAnnotation<Domain>()
val accessorName = /*domainAnnotation?.let { it.accessor } ?: */ "${operation.name}_${parameter.name}"
val found = kClass.functions.find {
(
it.name == accessorName ||
it.findAnnotation<DomainAccessor>()?.let { it.parameterName == parameter.name } ?: false
)
&& it.returnType.isSubtypeOf(Page::class.starProjectedType)
} as KFunction<Page<*>>?
return found?.let { kClass to found }
}

fun namespaceToPackageName(namespace : String) : String = kirraSpringApplication.javaPackages.find { it.endsWith(".${namespace}") } ?: namespace
fun getEntityClass(namespace: String, entityName: String): Class<BaseEntity>? {
Expand All @@ -90,12 +114,6 @@ class KirraSpringMetamodel {
}
fun getEntityClass(typeRef: TypeRef) = getEntityClass(typeRef.namespace, typeRef.typeName)

fun getServiceClass(typeRef : TypeRef) {
val serviceName = typeRef.typeName.decapitalize() + "Service"
val existingService = applicationContext.beanFactory.getSingleton(serviceName)

}

fun getEnumClass(typeRef: TypeRef) : Class<Enum<*>>? {
val packageName = namespaceToPackageName(typeRef.namespace)
val enumClassName = "${packageName}.${typeRef.typeName.replace('+', '$')}"
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/abstratt/kirra/spring/Named.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.abstratt.kirra.spring

import com.abstratt.kirra.Relationship
import java.time.LocalDate
import kotlin.reflect.KFunction
import kotlin.reflect.KFunction6

annotation class Named (
val label : String = "",
Expand All @@ -21,6 +24,18 @@ annotation class RelationshipAccessor(
val derived : Boolean = false
)

@Target(AnnotationTarget.FUNCTION)
annotation class DomainAccessor (
val methodName : String = "",
val parameterName : String = ""
)

@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Domain (
val accessor : String
)


@Target(AnnotationTarget.FUNCTION)
annotation class ImplementationOp

Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/abstratt/kirra/spring/helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ fun packageNameToNamespace(packageName : String) : String = packageName.split('.
fun getLabel(name: String): String =
StringUtils.splitByCharacterTypeCamelCase(name).map { it.capitalize() }.joinToString(" ", "", "")

fun <T> Boolean.ifTrue(block : () -> T) : T? = if (this) block() else null
14 changes: 9 additions & 5 deletions src/test/java/com/abstratt/kirra/spring/TestBase.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.abstratt.kirra.spring

import com.abstratt.kirra.Schema
import com.abstratt.kirra.spring.testing.sample.Customer
import com.abstratt.kirra.spring.testing.sample.Employee
import com.abstratt.kirra.spring.testing.sample.*
import com.abstratt.kirra.spring.userprofile.UserProfile
import org.junit.Before
import org.junit.runner.RunWith
Expand Down Expand Up @@ -35,10 +34,15 @@ abstract class TestBase {
@Autowired
protected lateinit var userProfileService: BaseService<UserProfile,*>
@Autowired()
private lateinit var customerService: BaseService<Customer,*>
protected lateinit var customerService: BaseService<Customer,*>
@Autowired
private lateinit var employeeService: BaseService<Employee,*>

protected lateinit var employeeService: BaseService<Employee,*>
@Autowired
protected lateinit var productService: BaseService<Product,*>
@Autowired
protected lateinit var categoryService: BaseService<Category,*>
@Autowired
protected lateinit var orderService: BaseService<Order,*>
protected var user1: UserProfile? = null
protected var user2: UserProfile? = null
protected var user3: UserProfile? = null
Expand Down
Loading

0 comments on commit ed6452e

Please sign in to comment.