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

pl.touk.nussknacker.engine.definition.clazz.ClassDefinitionDiscovery.scala Maven / Gradle / Ivy

The newest version!
package pl.touk.nussknacker.engine.definition.clazz

import com.typesafe.scalalogging.LazyLogging
import org.apache.commons.lang3.ClassUtils
import pl.touk.nussknacker.engine.api.generics.Parameter
import pl.touk.nussknacker.engine.api.typed.typing._
import pl.touk.nussknacker.engine.util.logging.ExecutionTimeMeasuring
import pl.touk.nussknacker.engine.variables.MetaVariables

import scala.collection.mutable

class ClassDefinitionDiscovery(classDefinitionExtractor: ClassDefinitionExtractor)
    extends LazyLogging
    with ExecutionTimeMeasuring {

  // TODO: this should be somewhere in utils??
  private val primitiveTypes: List[Class[_]] = List(
    java.lang.Boolean.TYPE,
    java.lang.Integer.TYPE,
    java.lang.Long.TYPE,
    java.lang.Float.TYPE,
    java.lang.Double.TYPE,
    java.lang.Byte.TYPE,
    java.lang.Short.TYPE,
    java.lang.Character.TYPE
  )

  // We have some types that can't be discovered based on model but can by provided using e.g. literals
  // TODO: what else should be here?
  // TODO: Here, this static list of mandatory classes, should be replaced by mechanizm that allows
  // to plug in mandatory classes from other modules, e.g. BaseKafkaInputMetaVariables from kafka-util
  private val mandatoryClasses = (Set(
    classOf[java.util.List[_]],
    classOf[java.util.Map[_, _]],
    classOf[java.util.Map.Entry[_, _]],
    classOf[java.math.BigDecimal],
    classOf[Number],
    classOf[String],
    classOf[MetaVariables],
    classOf[Any]
  ) ++
    // Literals for primitive types are wrapped to boxed representations
    primitiveTypes.map(ClassUtils.primitiveToWrapper)).map(Typed(_))

  def discoverClasses(classes: Iterable[Class[_]]): Set[ClassDefinition] = {
    discoverClassesFromTypes(classes.map(Typed(_)))
  }

  def discoverClassesFromTypes(
      types: Iterable[TypingResult]
  ): Set[ClassDefinition] = {
    // It's a deep first traversal - to avoid SOF we use mutable collection. It won't be easy to implement it using immutable collections
    val collectedSoFar = mutable.HashSet.empty[TypingResult]
    (types.flatMap(typesFromTypingResult) ++ mandatoryClasses).flatMap { cl =>
      classAndItsChildrenDefinitionIfNotCollectedSoFar(cl)(collectedSoFar, DiscoveryPath(List(Clazz(cl))))
    }.toSet
  }

  private def typesFromTypingResult(typingResult: TypingResult): Set[TypingResult] = typingResult match {
    case TypedObjectTypingResult(fields, clazz, _) =>
      typesFromTypedClass(clazz) ++ fields.values.flatMap(typesFromTypingResult)
    case typedObjectWithData: SingleTypingResult =>
      typesFromTypedClass(typedObjectWithData.runtimeObjType)
    case union: TypedUnion =>
      union.possibleTypes.toList.toSet.flatMap(typesFromTypingResult)
    case TypedNull =>
      Set.empty
    case Unknown =>
      Set.empty
  }

  private def typesFromTypedClass(typedClass: TypedClass): Set[TypingResult] =
    typedClass.params.flatMap(typesFromTypingResult).toSet + Typed(typedClass.klass)

  private def classAndItsChildrenDefinitionIfNotCollectedSoFar(
      typingResult: TypingResult
  )(collectedSoFar: mutable.Set[TypingResult], path: DiscoveryPath): Set[ClassDefinition] = {
    if (collectedSoFar.contains(typingResult)) {
      Set.empty
    } else {
      collectedSoFar += typingResult
      classAndItsChildrenDefinition(typingResult)(collectedSoFar, path)
    }
  }

  private def classAndItsChildrenDefinition(
      typingResult: TypingResult
  )(collectedSoFar: mutable.Set[TypingResult], path: DiscoveryPath): Set[ClassDefinition] = {
    typingResult match {
      case e: TypedClass =>
        val definitionsForClass = if (classDefinitionExtractor.isHidden(e.klass)) {
          Set.empty
        } else {
          val classDefinition = extractDefinitionWithLogging(e.klass)(path)
          definitionsFromMethods(classDefinition)(collectedSoFar, path) + classDefinition
        }
        definitionsForClass ++ definitionsFromGenericParameters(e)(collectedSoFar, path)
      case Unknown => Set(extractDefinitionWithLogging(classOf[Any])(path))
      case _       => Set.empty[ClassDefinition]
    }
  }

  private def definitionsFromGenericParameters(
      typedClass: TypedClass
  )(collectedSoFar: mutable.Set[TypingResult], path: DiscoveryPath): Set[ClassDefinition] = {
    typedClass.params.zipWithIndex.flatMap {
      case (k: TypedClass, idx) =>
        classAndItsChildrenDefinitionIfNotCollectedSoFar(k)(collectedSoFar, path.pushSegment(GenericParameter(k, idx)))
      case _ => Set.empty[ClassDefinition]
    }.toSet
  }

  private def definitionsFromMethods(
      classDefinition: ClassDefinition
  )(collectedSoFar: mutable.Set[TypingResult], path: DiscoveryPath): Set[ClassDefinition] = {
    def extractFromMethods(methods: Map[String, List[MethodDefinition]]) =
      methods.values.flatten
        .flatMap(_.signatures.toList.map(_.result))
        .flatMap { kl =>
          classAndItsChildrenDefinitionIfNotCollectedSoFar(kl)(collectedSoFar, path.pushSegment(MethodReturnType(kl)))
          // TODO verify if parameters are need and if they are not, remove this
          //        ++ kl.parameters.flatMap(p => classAndItsChildrenDefinition(p.refClazz)(collectedSoFar, path.pushSegment(MethodParameter(p))))
        }
        .toSet

    extractFromMethods(classDefinition.methods) ++ extractFromMethods(classDefinition.staticMethods)
  }

  private def extractDefinitionWithLogging(
      clazz: Class[_]
  )(path: DiscoveryPath) = {
    def message = if (logger.underlying.isTraceEnabled) path.print else clazz.getName
    measure(message) {
      classDefinitionExtractor.extract(clazz)
    }
  }

  private case class DiscoveryPath(path: List[DiscoverySegment]) {
    def pushSegment(seg: DiscoverySegment): DiscoveryPath = DiscoveryPath(seg :: path)

    def print: String = {
      path.reverse.map(_.print).mkString(" > ")
    }

  }

  private sealed trait DiscoverySegment {
    def print: String

    protected def classNameWithStrippedPackages(cl: TypingResult): String = cl match {
      case TypedClass(klass, _) => klass.getCanonicalName.replaceAll("(.).*?\\.", "$1.")
      // In fact, should not happen except Unknown...
      case other => other.display
    }

  }

  private case class Clazz(cl: TypingResult) extends DiscoverySegment {
    override def print: String = classNameWithStrippedPackages(cl)
  }

  private case class GenericParameter(cl: TypingResult, ix: Int) extends DiscoverySegment {
    override def print: String = s"[$ix]${classNameWithStrippedPackages(cl)}"
  }

  private case class MethodReturnType(m: TypingResult) extends DiscoverySegment {
    override def print: String = s"ret(${classNameWithStrippedPackages(m)})"
  }

  private case class MethodParameter(p: Parameter) extends DiscoverySegment {
    override def print: String = s"${p.name}: ${classNameWithStrippedPackages(p.refClazz)}"
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy