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

pl.touk.nussknacker.engine.extension.CastOrConversionExt.scala Maven / Gradle / Ivy

The newest version!
package pl.touk.nussknacker.engine.extension

import cats.data.ValidatedNel
import cats.implicits.catsSyntaxValidatedId
import org.apache.commons.lang3.LocaleUtils
import org.springframework.util.StringUtils
import pl.touk.nussknacker.engine.api.generics.{GenericFunctionTypingError, MethodTypeInfo, Parameter}
import pl.touk.nussknacker.engine.api.typed.typing
import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypedObjectWithValue, TypingResult, Unknown}
import pl.touk.nussknacker.engine.definition.clazz.{ClassDefinitionSet, FunctionalMethodDefinition, MethodDefinition}
import pl.touk.nussknacker.engine.extension.CastOrConversionExt.{
  canBeMethodName,
  getConversion,
  toMethodName,
  toOrNullMethodName
}
import pl.touk.nussknacker.engine.extension.ExtensionMethod.SingleArg
import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap
import pl.touk.nussknacker.engine.util.classes.Extensions.{ClassExtensions, ClassesExtensions}

import java.lang.{Boolean => JBoolean}
import java.nio.charset.Charset
import java.time.chrono.{ChronoLocalDate, ChronoLocalDateTime}
import java.time.{LocalDate, LocalDateTime, LocalTime, ZoneId, ZoneOffset}
import java.util.{Currency, UUID}
import scala.util.Try

// todo: lbg - add casting methods to UTIL
class CastOrConversionExt(classesBySimpleName: Map[String, Class[_]]) {
  private val castException = new ClassCastException(s"Cannot cast value to given class")

  private val methodRegistry: Map[String, ExtensionMethod[_]] = Map(
    canBeMethodName    -> SingleArg(canBe),
    toMethodName       -> SingleArg(to),
    toOrNullMethodName -> SingleArg(toOrNull),
  )

  private def canBe(target: Any, className: String): Boolean =
    getClass(className).exists(clazz => clazz.isAssignableFrom(target.getClass)) ||
      getConversion(className).exists(_.canConvert(target))

  private def to(target: Any, className: String): Any =
    orElse(tryCast(target, className), tryConvert(target, className)) match {
      case Right(value) => value
      case Left(ex) => throw new IllegalStateException(s"Cannot cast or convert value: $target to: '$className'", ex)
    }

  private def toOrNull(target: Any, className: String): Any =
    orElse(tryCast(target, className), tryConvert(target, className))
      .getOrElse(null)

  private def tryCast(target: Any, className: String): Either[Throwable, Any] = getClass(className) match {
    case Some(clazz) if clazz.isInstance(target) => Try(clazz.cast(target)).toEither
    case _                                       => Left(castException)
  }

  private def getClass(className: String): Option[Class[_]] =
    classesBySimpleName.get(className.toLowerCase())

  private def tryConvert(target: Any, className: String): Either[Throwable, Any] =
    getConversion(className)
      .flatMap(_.convertEither(target))

  // scala 2.12 does not support either.orElse
  private def orElse(e1: Either[Throwable, Any], e2: => Either[Throwable, Any]): Either[Throwable, Any] =
    e1 match {
      case Left(_)      => e2
      case r @ Right(_) => r
    }

}

object CastOrConversionExt extends ExtensionMethodsDefinition {
  private val stringClass                   = classOf[String]
  private[extension] val canBeMethodName    = "canBe"
  private[extension] val toMethodName       = "to"
  private[extension] val orNullSuffix       = "OrNull"
  private[extension] val toOrNullMethodName = toMethodName + orNullSuffix
  private val castOrConversionMethods       = Set(canBeMethodName, toMethodName, toOrNullMethodName)

  private val conversionsRegistry: List[Conversion[_ >: Null <: AnyRef]] = List(
    ToLongConversion,
    ToDoubleConversion,
    ToBigDecimalConversion,
    ToBooleanConversion,
    ToStringConversion,
    ToMapConversion,
    ToListConversion,
    ToByteConversion,
    ToShortConversion,
    ToIntegerConversion,
    ToFloatConversion,
    ToBigIntegerConversion,
    new FromStringConversion(ZoneOffset.of),
    new FromStringConversion(ZoneId.of),
    new FromStringConversion((source: String) => {
      val locale = StringUtils.parseLocale(source)
      assert(LocaleUtils.isAvailableLocale(locale)) // without this check even "qwerty" is considered a Locale
      locale
    }),
    new FromStringConversion(Charset.forName),
    new FromStringConversion(Currency.getInstance),
    new FromStringConversion[UUID]((source: String) =>
      if (StringUtils.hasLength(source)) UUID.fromString(source.trim) else null
    ),
    new FromStringConversion(LocalTime.parse),
    new FromStringConversion(LocalDate.parse),
    new FromStringConversion(LocalDateTime.parse),
    new FromStringConversion[ChronoLocalDate](LocalDate.parse),
    new FromStringConversion[ChronoLocalDateTime[_]](LocalDateTime.parse)
  )

  private val conversionsByType: Map[String, Conversion[_ >: Null <: AnyRef]] = conversionsRegistry
    .flatMap(c => c.resultTypeClass.classByNameAndSimpleNameLowerCase().map(n => n._1 -> c))
    .toMap

  def isCastOrConversionMethod(methodName: String): Boolean =
    castOrConversionMethods.contains(methodName)

  def allowedConversions(clazz: Class[_]): List[Conversion[_]] =
    conversionsRegistry.filter(_.appliesToConversion(clazz))

  // Convert methods should visible in runtime for every class because we allow invoke convert methods on an unknown
  // object in Typer, but in the runtime the same type could be known and that's why should add convert method to an
  // every class.
  override def findMethod(
      clazz: Class[_],
      methodName: String,
      argsSize: Int,
      set: ClassDefinitionSet
  ): Option[ExtensionMethod[_]] =
    new CastOrConversionExt(set.classDefinitionsMap.keySet.classesByNamesAndSimpleNamesLowerCase()).methodRegistry
      .findMethod(methodName, argsSize)

  override def extractDefinitions(clazz: Class[_], set: ClassDefinitionSet): Map[String, List[MethodDefinition]] = {
    val castAllowedClasses = clazz.findAllowedClassesForCastParameter(set).mapValuesNow(_.clazzName)
    val isConvertibleClass = conversionsRegistry.exists(_.appliesToConversion(clazz))
    if (castAllowedClasses.nonEmpty || isConvertibleClass) {
      definitions(castAllowedClasses)
    } else {
      Map.empty
    }
  }

  private def getConversion(className: String): Either[Throwable, Conversion[_]] =
    conversionsByType.get(className.toLowerCase) match {
      case Some(conversion) => Right(conversion)
      case None             => Left(new IllegalArgumentException(s"Conversion for class $className not found"))
    }

  private def definitions(allowedClasses: Map[Class[_], TypingResult]): Map[String, List[MethodDefinition]] =
    List(
      FunctionalMethodDefinition(
        (target, params) => canConvertToTyping(allowedClasses)(target, params),
        methodTypeInfoWithStringParam(Typed.typedClass[JBoolean]),
        canBeMethodName,
        Some("Checks if a type can be converted to a given class")
      ),
      FunctionalMethodDefinition(
        (target, params) => convertToTyping(allowedClasses)(target, params),
        methodTypeInfoWithStringParam(Unknown),
        toMethodName,
        Some("Converts a type to a given class or throws exception if type cannot be converted.")
      ),
      FunctionalMethodDefinition(
        (target, params) => convertToTyping(allowedClasses)(target, params),
        methodTypeInfoWithStringParam(Unknown),
        toOrNullMethodName,
        Some("Converts a type to a given class or return null if type cannot be converted.")
      ),
    ).groupBy(_.name)

  private def convertToTyping(allowedClasses: Map[Class[_], TypingResult])(
      invocationTarget: TypingResult,
      arguments: List[TypingResult]
  ): ValidatedNel[GenericFunctionTypingError, typing.TypingResult] = arguments match {
    case TypedObjectWithValue(_, clazzName: String) :: Nil =>
      allowedClasses
        .find(_._1.equalsScalaClassNameIgnoringCase(clazzName))
        .map(_._2.validNel)
        .orElse(getConversion(clazzName).map(_.typingFunction(invocationTarget)).toOption) match {
        case Some(result) => result
        case _ => GenericFunctionTypingError.OtherError(s"Cannot cast or convert to: '$clazzName'").invalidNel
      }
    case _ => GenericFunctionTypingError.ArgumentTypeError.invalidNel
  }

  private def canConvertToTyping(allowedClasses: Map[Class[_], TypingResult])(
      invocationTarget: TypingResult,
      arguments: List[TypingResult]
  ): ValidatedNel[GenericFunctionTypingError, TypingResult] =
    convertToTyping(allowedClasses)(invocationTarget, arguments).map(_ => Typed.typedClass[Boolean])

  private def methodTypeInfoWithStringParam(result: TypingResult) = MethodTypeInfo(
    noVarArgs = List(
      Parameter("className", Typed.genericTypeClass(stringClass, Nil))
    ),
    varArg = None,
    result = result
  )

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy