All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
kalix.javasdk.impl.ComponentDescriptorFactory.scala Maven / Gradle / Ivy
/*
* Copyright 2021 Lightbend Inc.
*
* 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 kalix.javasdk.impl
import java.lang.annotation.Annotation
import java.lang.reflect.AnnotatedElement
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.lang.reflect.ParameterizedType
import scala.reflect.ClassTag
import kalix.DirectDestination
import kalix.DirectSource
import kalix.EventDestination
import kalix.EventSource
import kalix.Eventing
import kalix.JwtMethodOptions
import kalix.MethodOptions
import kalix.ServiceEventing
import kalix.ServiceEventingOut
import kalix.javasdk.action.Action
import kalix.javasdk.annotations.Publish
import kalix.javasdk.annotations.Subscribe
import kalix.javasdk.view.View
import kalix.javasdk.annotations.Acl
import kalix.javasdk.annotations.EntityType
import kalix.javasdk.annotations.JWT
import kalix.javasdk.annotations.Table
import kalix.javasdk.annotations.TypeId
import kalix.javasdk.annotations.ViewId
import kalix.javasdk.eventsourcedentity.EventSourcedEntity
import kalix.javasdk.impl.reflection.CombinedSubscriptionServiceMethod
import kalix.javasdk.impl.reflection.KalixMethod
import kalix.javasdk.impl.reflection.NameGenerator
import kalix.javasdk.valueentity.ValueEntity
// TODO: abstract away spring dependency
import org.springframework.core.annotation.AnnotatedElementUtils
import org.springframework.web.bind.annotation.RequestMapping
private[impl] object ComponentDescriptorFactory {
implicit class MethodOps(javaMethod: Method) {
def isPublic = Modifier.isPublic(javaMethod.getModifiers)
def hasAnnotation[A <: Annotation](implicit ev: ClassTag[A]) =
javaMethod.getAnnotation(ev.runtimeClass.asInstanceOf[Class[Annotation]]) != null
}
def hasRestAnnotation(javaMethod: Method): Boolean = {
val restAnnotation = AnnotatedElementUtils.findMergedAnnotation(javaMethod, classOf[RequestMapping])
javaMethod.isPublic && restAnnotation != null
}
def hasAcl(javaMethod: Method): Boolean =
javaMethod.isPublic && javaMethod.hasAnnotation[Acl]
def hasValueEntitySubscription(clazz: Class[_]): Boolean =
Modifier.isPublic(clazz.getModifiers) &&
clazz.getAnnotation(classOf[Subscribe.ValueEntity]) != null
def hasValueEntitySubscription(javaMethod: Method): Boolean =
javaMethod.isPublic && javaMethod.hasAnnotation[Subscribe.ValueEntity]
def hasEventSourcedEntitySubscription(javaMethod: Method): Boolean =
javaMethod.isPublic && javaMethod.hasAnnotation[Subscribe.EventSourcedEntity]
def hasEventSourcedEntitySubscription(clazz: Class[_]): Boolean =
Modifier.isPublic(clazz.getModifiers) &&
clazz.getAnnotation(classOf[Subscribe.EventSourcedEntity]) != null
def streamSubscription(clazz: Class[_]): Option[Subscribe.Stream] =
if (Modifier.isPublic(clazz.getModifiers))
Option(clazz.getAnnotation(classOf[Subscribe.Stream]))
else
None
def hasSubscription(javaMethod: Method): Boolean = {
hasValueEntitySubscription(javaMethod) ||
hasEventSourcedEntitySubscription(javaMethod) ||
hasTopicSubscription(javaMethod)
}
def hasSubscription(clazz: Class[_]): Boolean = {
hasValueEntitySubscription(clazz) ||
hasEventSourcedEntitySubscription(clazz) ||
hasTopicSubscription(clazz) ||
hasStreamSubscription(clazz)
}
def valueEntitySubscription(clazz: Class[_]): Option[Subscribe.ValueEntity] =
if (Modifier.isPublic(clazz.getModifiers))
Option(clazz.getAnnotation(classOf[Subscribe.ValueEntity]))
else
None
def eventSourcedEntitySubscription(clazz: Class[_]): Option[Subscribe.EventSourcedEntity] =
if (Modifier.isPublic(clazz.getModifiers))
Option(clazz.getAnnotation(classOf[Subscribe.EventSourcedEntity]))
else
None
def topicSubscription(clazz: Class[_]): Option[Subscribe.Topic] =
if (Modifier.isPublic(clazz.getModifiers))
Option(clazz.getAnnotation(classOf[Subscribe.Topic]))
else
None
def hasActionOutput(javaMethod: Method): Boolean = {
if (Modifier.isPublic(javaMethod.getModifiers)) {
javaMethod.getGenericReturnType match {
case p: ParameterizedType => p.getRawType.equals(classOf[Action.Effect[_]])
case _ => false
}
} else {
false
}
}
def hasUpdateEffectOutput(javaMethod: Method): Boolean = {
if (Modifier.isPublic(javaMethod.getModifiers)) {
javaMethod.getGenericReturnType match {
case p: ParameterizedType => p.getRawType.equals(classOf[View.UpdateEffect[_]])
case _ => false
}
} else {
false
}
}
def hasTopicSubscription(javaMethod: Method): Boolean =
javaMethod.isPublic && javaMethod.hasAnnotation[Subscribe.Topic]
def hasHandleDeletes(javaMethod: Method): Boolean = {
val ann = javaMethod.getAnnotation(classOf[Subscribe.ValueEntity])
Modifier.isPublic(javaMethod.getModifiers) && ann != null && ann.handleDeletes()
}
def hasTopicSubscription(clazz: Class[_]): Boolean =
Modifier.isPublic(clazz.getModifiers) &&
clazz.getAnnotation(classOf[Subscribe.Topic]) != null
def hasStreamSubscription(clazz: Class[_]): Boolean =
Modifier.isPublic(clazz.getModifiers) &&
clazz.getAnnotation(classOf[Subscribe.Stream]) != null
def hasTopicPublication(javaMethod: Method): Boolean =
javaMethod.isPublic && javaMethod.hasAnnotation[Publish.Topic]
def hasJwtMethodOptions(javaMethod: Method): Boolean =
javaMethod.isPublic && javaMethod.hasAnnotation[JWT]
def readTypeIdValue(annotated: AnnotatedElement) =
Option(annotated.getAnnotation(classOf[TypeId]))
.map(_.value())
.getOrElse {
// assuming that if TypeId is not in use, EntityType will
annotated.getAnnotation(classOf[EntityType]).value()
}
def findEventSourcedEntityType(javaMethod: Method): String = {
val ann = javaMethod.getAnnotation(classOf[Subscribe.EventSourcedEntity])
readTypeIdValue(ann.value())
}
def findEventSourcedEntityClass(javaMethod: Method): Class[_ <: EventSourcedEntity[_, _]] = {
val ann = javaMethod.getAnnotation(classOf[Subscribe.EventSourcedEntity])
ann.value()
}
private def findValueEntityClass(javaMethod: Method): Class[_ <: ValueEntity[_]] = {
val ann = javaMethod.getAnnotation(classOf[Subscribe.ValueEntity])
ann.value()
}
def findSubscriptionSourceName(javaMethod: Method): String = {
if (hasValueEntitySubscription(javaMethod)) {
findValueEntityClass(javaMethod).getName
} else if (hasEventSourcedEntitySubscription(javaMethod)) {
findEventSourcedEntityClass(javaMethod).getName
} else if (hasTopicSubscription(javaMethod)) {
"Topic-" + findSubscriptionTopicName(javaMethod)
} else {
throw new IllegalStateException("Unsupported source for " + javaMethod.getName)
}
}
def findEventSourcedEntityType(clazz: Class[_]): String = {
val ann = clazz.getAnnotation(classOf[Subscribe.EventSourcedEntity])
readTypeIdValue(ann.value())
}
def findValueEntityType(javaMethod: Method): String = {
val ann = javaMethod.getAnnotation(classOf[Subscribe.ValueEntity])
readTypeIdValue(ann.value())
}
def findValueEntityType(component: Class[_]): String = {
val ann = component.getAnnotation(classOf[Subscribe.ValueEntity])
readTypeIdValue(ann.value())
}
def findHandleDeletes(javaMethod: Method): Boolean = {
val ann = javaMethod.getAnnotation(classOf[Subscribe.ValueEntity])
ann.handleDeletes()
}
def findHandleDeletes(component: Class[_]): Boolean = {
val ann = component.getAnnotation(classOf[Subscribe.ValueEntity])
ann.handleDeletes()
}
def findSubscriptionTopicName(javaMethod: Method): String = {
val ann = javaMethod.getAnnotation(classOf[Subscribe.Topic])
ann.value()
}
def findSubscriptionTopicName(clazz: Class[_]): String = {
val ann = clazz.getAnnotation(classOf[Subscribe.Topic])
ann.value()
}
def findSubscriptionConsumerGroup(javaMethod: Method): String = {
val ann = javaMethod.getAnnotation(classOf[Subscribe.Topic])
ann.consumerGroup()
}
private def findSubscriptionConsumerGroup(clazz: Class[_]): String = {
val ann = clazz.getAnnotation(classOf[Subscribe.Topic])
ann.consumerGroup()
}
def findPublicationTopicName(javaMethod: Method): String = {
val ann = javaMethod.getAnnotation(classOf[Publish.Topic])
ann.value()
}
def hasIgnoreForTopic(clazz: Class[_]): Boolean = {
val ann = clazz.getAnnotation(classOf[Subscribe.Topic])
ann.ignoreUnknown()
}
def hasIgnoreForEventSourcedEntity(clazz: Class[_]): Boolean = {
val ann = clazz.getAnnotation(classOf[Subscribe.EventSourcedEntity])
ann.ignoreUnknown()
}
def findIgnore(clazz: Class[_]): Boolean = {
if (hasTopicSubscription(clazz)) hasIgnoreForTopic(clazz)
else if (hasEventSourcedEntitySubscription(clazz)) hasIgnoreForEventSourcedEntity(clazz)
else false
}
def jwtMethodOptions(javaMethod: Method): JwtMethodOptions = {
val ann = javaMethod.getAnnotation(classOf[JWT])
val jwt = JwtMethodOptions.newBuilder()
ann
.validate()
.map(springValidate => jwt.addValidate(JwtMethodOptions.JwtMethodMode.forNumber(springValidate.ordinal())))
ann.bearerTokenIssuer().map(jwt.addBearerTokenIssuer)
jwt.build()
}
def eventingInForValueEntity(javaMethod: Method): Eventing = {
val eventSource: EventSource = valueEntityEventSource(javaMethod)
Eventing.newBuilder().setIn(eventSource).build()
}
def valueEntityEventSource(javaMethod: Method) = {
val entityType = findValueEntityType(javaMethod)
EventSource
.newBuilder()
.setValueEntity(entityType)
.setHandleDeletes(findHandleDeletes(javaMethod))
.build()
}
def topicEventDestination(javaMethod: Method): Option[EventDestination] = {
if (hasTopicPublication(javaMethod)) {
val topicName = findPublicationTopicName(javaMethod)
Some(EventDestination.newBuilder().setTopic(topicName).build())
} else {
None
}
}
def eventingInForEventSourcedEntity(javaMethod: Method): Eventing = {
val eventSource: EventSource = eventSourceEntityEventSource(javaMethod)
// ignore in method must be always false
Eventing.newBuilder().setIn(eventSource).build()
}
def eventSourceEntityEventSource(javaMethod: Method) = {
val entityType = findEventSourcedEntityType(javaMethod)
EventSource.newBuilder().setEventSourcedEntity(entityType).build()
}
def eventingInForEventSourcedEntity(clazz: Class[_]): Eventing = {
val entityType = findEventSourcedEntityType(clazz)
val eventSource = EventSource.newBuilder().setEventSourcedEntity(entityType).build()
Eventing.newBuilder().setIn(eventSource).build()
}
def eventingInForTopic(clazz: Class[_]): Eventing = {
Eventing.newBuilder().setIn(topicEventSource(clazz)).build()
}
def eventingInForTopic(javaMethod: Method): Eventing = {
Eventing.newBuilder().setIn(topicEventSource(javaMethod)).build()
}
def eventingInForValueEntityServiceLevel(clazz: Class[_]): Option[kalix.ServiceOptions] = {
valueEntitySubscription(clazz).map { _ =>
val entityType = findValueEntityType(clazz)
val in = EventSource.newBuilder().setValueEntity(entityType)
val eventing = ServiceEventing.newBuilder().setIn(in)
kalix.ServiceOptions.newBuilder().setEventing(eventing).build()
}
}
def eventingInForEventSourcedEntityServiceLevel(clazz: Class[_]): Option[kalix.ServiceOptions] = {
eventSourcedEntitySubscription(clazz).map { _ =>
val entityType = findEventSourcedEntityType(clazz)
val in = EventSource.newBuilder().setEventSourcedEntity(entityType)
val eventing = ServiceEventing.newBuilder().setIn(in)
kalix.ServiceOptions.newBuilder().setEventing(eventing).build()
}
}
def eventingInForTopicServiceLevel(clazz: Class[_]): Option[kalix.ServiceOptions] = {
topicSubscription(clazz).map { ann =>
val in = EventSource.newBuilder().setTopic(ann.value()).setConsumerGroup(ann.consumerGroup())
val eventing = ServiceEventing.newBuilder().setIn(in)
kalix.ServiceOptions.newBuilder().setEventing(eventing).build()
}
}
def topicEventSource(javaMethod: Method): EventSource = {
val topicName = findSubscriptionTopicName(javaMethod)
val consumerGroup = findSubscriptionConsumerGroup(javaMethod)
EventSource.newBuilder().setTopic(topicName).setConsumerGroup(consumerGroup).build()
}
def topicEventSource(clazz: Class[_]): EventSource = {
val topicName = findSubscriptionTopicName(clazz)
val consumerGroup = findSubscriptionConsumerGroup(clazz)
EventSource.newBuilder().setTopic(topicName).setConsumerGroup(consumerGroup).build()
}
def eventingOutForTopic(javaMethod: Method): Option[Eventing] = {
topicEventDestination(javaMethod).map(eventSource => Eventing.newBuilder().setOut(eventSource).build())
}
def eventingInForValueEntity(entityType: String, handleDeletes: Boolean): Eventing = {
val eventSource = EventSource
.newBuilder()
.setValueEntity(entityType)
.setHandleDeletes(handleDeletes)
.build()
Eventing.newBuilder().setIn(eventSource).build()
}
def subscribeToEventStream(component: Class[_]): Option[kalix.ServiceOptions] = {
Option(component.getAnnotation(classOf[Subscribe.Stream])).map { streamAnn =>
val direct = DirectSource
.newBuilder()
.setEventStreamId(streamAnn.id())
.setService(streamAnn.service())
val in = EventSource
.newBuilder()
.setDirect(direct)
val eventing =
ServiceEventing
.newBuilder()
.setIn(in)
kalix.ServiceOptions
.newBuilder()
.setEventing(eventing)
.build()
}
}
def publishToEventStream(component: Class[_]): Option[kalix.ServiceOptions] = {
Option(component.getAnnotation(classOf[Publish.Stream])).map { streamAnn =>
val direct = DirectDestination
.newBuilder()
.setEventStreamId(streamAnn.id())
val out = ServiceEventingOut
.newBuilder()
.setDirect(direct)
val eventing =
ServiceEventing
.newBuilder()
.setOut(out)
kalix.ServiceOptions
.newBuilder()
.setEventing(eventing)
.build()
}
}
// TODO: add more validations here
// we should let users know if components are missing required annotations,
// eg: Workflow and Entities require @TypeId, View requires @Table and @Subscription
def getFactoryFor(component: Class[_]): ComponentDescriptorFactory = {
if (component.getAnnotation(classOf[TypeId]) != null || component.getAnnotation(classOf[EntityType]) != null)
EntityDescriptorFactory
else if (component.getAnnotation(classOf[Table]) != null || component.getAnnotation(classOf[ViewId]) != null)
ViewDescriptorFactory
else
ActionDescriptorFactory
}
def combineByES(
subscriptions: Seq[KalixMethod],
messageCodec: JsonMessageCodec,
component: Class[_]): Seq[KalixMethod] = {
def groupByES(methods: Seq[KalixMethod]): Map[String, Seq[KalixMethod]] = {
val withEventSourcedIn = methods.filter(kalixMethod =>
kalixMethod.methodOptions.exists(option =>
option.hasEventing && option.getEventing.hasIn && option.getEventing.getIn.hasEventSourcedEntity))
//Assuming there is only one eventing.in annotation per method, therefore head is as good as any other
withEventSourcedIn.groupBy(m => m.methodOptions.head.getEventing.getIn.getEventSourcedEntity)
}
combineBy("ES", groupByES(subscriptions), messageCodec, component)
}
def combineByTopic(
kalixMethods: Seq[KalixMethod],
messageCodec: JsonMessageCodec,
component: Class[_]): Seq[KalixMethod] = {
def groupByTopic(methods: Seq[KalixMethod]): Map[String, Seq[KalixMethod]] = {
val withTopicIn = methods.filter(kalixMethod =>
kalixMethod.methodOptions.exists(option =>
option.hasEventing && option.getEventing.hasIn && option.getEventing.getIn.hasTopic))
//Assuming there is only one topic annotation per method, therefore head is as good as any other
withTopicIn.groupBy(m => m.methodOptions.head.getEventing.getIn.getTopic)
}
combineBy("Topic", groupByTopic(kalixMethods), messageCodec, component)
}
def combineBy(
sourceName: String,
groupedSubscriptions: Map[String, Seq[KalixMethod]],
messageCodec: JsonMessageCodec,
component: Class[_]): Seq[KalixMethod] = {
groupedSubscriptions.collect {
case (source, kMethods) if kMethods.size > 1 =>
val methodsMap =
kMethods.flatMap { k =>
val methodParameterTypes = k.serviceMethod.javaMethodOpt.get.getParameterTypes
// it is safe to pick the last parameter. An action has one and View has two. In the View always the last is the event
val eventParameter = methodParameterTypes.last
messageCodec.typeUrlsFor(eventParameter).map(typeUrl => (typeUrl, k.serviceMethod.javaMethodOpt.get))
}.toMap
KalixMethod(
CombinedSubscriptionServiceMethod(
component.getName,
"KalixSyntheticMethodOn" + sourceName + escapeMethodName(source.capitalize),
methodsMap))
.withKalixOptions(kMethods.head.methodOptions)
case (_, kMethod +: Nil) => kMethod
}.toSeq
}
private[impl] def escapeMethodName(value: String): String = {
value.replaceAll("[\\._\\-]", "")
}
private[impl] def buildJWTOptions(method: Method): Option[MethodOptions] = {
Option.when(hasJwtMethodOptions(method)) {
kalix.MethodOptions.newBuilder().setJwt(jwtMethodOptions(method)).build()
}
}
private[impl] def buildEventingOutOptions(method: Method): Option[MethodOptions] = {
eventingOutForTopic(method).map(eventingOut => kalix.MethodOptions.newBuilder().setEventing(eventingOut).build())
}
private[impl] def jwtOptions(method: Method): Option[JwtMethodOptions] = {
if (hasJwtMethodOptions(method)) {
Some(jwtMethodOptions(method))
} else {
None
}
}
}
private[impl] trait ComponentDescriptorFactory {
/**
* Inspect the component class (type), validate the annotations/methods and build a component descriptor for it.
*/
def buildDescriptorFor(
componentClass: Class[_],
messageCodec: JsonMessageCodec,
nameGenerator: NameGenerator): ComponentDescriptor
}
/**
* Thrown when the component has incorrect annotations
*/
final case class InvalidComponentException(message: String) extends RuntimeException(message)