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

pl.touk.nussknacker.engine.spel.SpelExpression.scala Maven / Gradle / Ivy

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

import cats.data.Validated.Valid
import cats.data.{NonEmptyList, Validated, ValidatedNel}
import com.typesafe.scalalogging.LazyLogging
import org.apache.commons.lang3.StringUtils
import org.springframework.expression._
import org.springframework.expression.common.{CompositeStringExpression, LiteralExpression}
import org.springframework.expression.spel.ast.SpelNodeImpl
import org.springframework.expression.spel.{
  SpelCompilerMode,
  SpelEvaluationException,
  SpelParserConfiguration,
  standard
}
import pl.touk.nussknacker.engine.api.Context
import pl.touk.nussknacker.engine.api.context.ValidationContext
import pl.touk.nussknacker.engine.api.dict.DictRegistry
import pl.touk.nussknacker.engine.api.exception.NonTransientException
import pl.touk.nussknacker.engine.api.generics.ExpressionParseError
import pl.touk.nussknacker.engine.api.typed.typing
import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypingResult}
import pl.touk.nussknacker.engine.definition.clazz.ClassDefinitionSet
import pl.touk.nussknacker.engine.definition.globalvariables.ExpressionConfigDefinition
import pl.touk.nussknacker.engine.dict.{KeysDictTyper, LabelsDictTyper}
import pl.touk.nussknacker.engine.expression.NullExpression
import pl.touk.nussknacker.engine.expression.parse.{CompiledExpression, ExpressionParser, TypedExpression}
import pl.touk.nussknacker.engine.graph.expression.Expression.Language
import pl.touk.nussknacker.engine.graph.expression.{Expression => GraphExpression}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.ExpressionCompilationError
import pl.touk.nussknacker.engine.spel.SpelExpressionParser.Flavour
import pl.touk.nussknacker.engine.spel.internal.EvaluationContextPreparer

import scala.util.control.NonFatal

/**
  * Workaround for Spel compilation problem when expression's underlying class changes.
  * Spel tries to explicitly cast result of compiled expression to a class that has been cached during the compilation.
  *
  * Problematic scenario:
  * In case compilation occurs with type ArrayList and during evaluation a List is provided ClassCastException is thrown.
  * Workaround:
  * In such case we try to parse and compile expression again.
  *
  * Possible problems:
  * - unless Expression is marked @volatile multiple threads might parse it on their own,
  * - performance problem might occur if the ClassCastException is thrown often (e. g. for consecutive calls to getValue)
  */
final case class ParsedSpelExpression(
    original: String,
    parser: () => ValidatedNel[ExpressionParseError, Expression],
    initial: Expression
) extends LazyLogging {
  @volatile var parsed: Expression = initial

  def getValue[T](context: EvaluationContext, desiredResultType: Class[_]): T = {
    def value(): T = parsed.getValue(context, desiredResultType).asInstanceOf[T]

    try {
      value()
    } catch {
      case e: SpelEvaluationException if Option(e.getCause).exists(_.isInstanceOf[ClassCastException]) =>
        logger.warn("Error during expression evaluation '{}': {}. Trying to compile", original, e.getMessage)
        forceParse()
        value()
    }
  }

  def forceParse(): Unit = {
    // we already parsed this expression successfully, so reparsing should NOT fail
    parsed = parser().getOrElse(throw new RuntimeException(s"Failed to reparse $original - this should not happen!"))
  }

}

class SpelExpressionEvaluationException(val expression: String, val ctxId: String, cause: Throwable)
    extends NonTransientException(
      expression,
      s"Expression [$expression] evaluation failed, message: ${cause.getMessage}",
      cause = cause
    )

class SpelExpression(
    parsed: ParsedSpelExpression,
    expectedReturnType: TypingResult,
    flavour: Flavour,
    evaluationContextPreparer: EvaluationContextPreparer
) extends CompiledExpression
    with LazyLogging {

  override val original: String = parsed.original

  override val language: Language = flavour.languageId

  private val expectedClass =
    expectedReturnType match {
      case r: SingleTypingResult =>
        r.runtimeObjType.klass
      case _ =>
        // TODO: what should happen here?
        classOf[Any]
    }

  // TODO: better interoperability with scala type, mainly: scala.math.BigDecimal, scala.math.BigInt and collections
  override def evaluate[T](ctx: Context, globals: Map[String, Any]): T = logOnException(ctx) {
    if (expectedClass == classOf[SpelExpressionRepr]) {
      return SpelExpressionRepr(parsed.parsed, ctx, globals, original).asInstanceOf[T]
    }
    val evaluationContext = evaluationContextPreparer.prepareEvaluationContext(ctx, globals)
    parsed.getValue[T](evaluationContext, expectedClass)
  }

  private def logOnException[A](ctx: Context)(block: => A): A = {
    try {
      block
    } catch {
      case NonFatal(e) =>
        logger.info(
          s"Expression evaluation failed. Original {}, ctxId: {}, message: {}",
          original,
          ctx.id,
          e.getMessage
        )
        // we log twice here because LazyLogging cannot print context and stacktrace at the same time
        logger.debug("Expression evaluation failed. Original: {}. Context: {}", original, ctx)
        logger.debug("Expression evaluation failed", e)
        throw new SpelExpressionEvaluationException(original, ctx.id, e)
    }
  }

}

class SpelExpressionParser(
    parser: org.springframework.expression.spel.standard.SpelExpressionParser,
    validator: SpelExpressionValidator,
    dictRegistry: DictRegistry,
    enableSpelForceCompile: Boolean,
    flavour: Flavour,
    prepareEvaluationContext: EvaluationContextPreparer
) extends ExpressionParser {

  import pl.touk.nussknacker.engine.spel.SpelExpressionParser._

  override final val languageId: Language = flavour.languageId

  override def parseWithoutContextValidation(
      original: String,
      expectedType: TypingResult
  ): ValidatedNel[ExpressionParseError, CompiledExpression] = {
    if (shouldUseNullExpression(original)) {
      Valid(NullExpression(original, flavour))
    } else {
      baseParse(original).map { parsed =>
        expression(ParsedSpelExpression(original, () => baseParse(original), parsed), expectedType)
      }
    }
  }

  override def parse(
      original: String,
      ctx: ValidationContext,
      expectedType: TypingResult
  ): ValidatedNel[ExpressionParseError, TypedExpression] = {
    if (shouldUseNullExpression(original)) {
      Valid(
        TypedExpression(
          NullExpression(original, flavour),
          SpelExpressionTypingInfo(Map.empty, typing.TypedNull)
        )
      )
    } else {
      baseParse(original)
        .andThen { parsed =>
          validator.validate(parsed, ctx, expectedType).map((_, parsed))
        }
        .map { case (combinedResult, parsed) =>
          TypedExpression(
            expression(ParsedSpelExpression(original, () => baseParse(original), parsed), expectedType),
            combinedResult.typingInfo
          )
        }
    }
  }

  private def shouldUseNullExpression(original: String): Boolean = flavour != Template && StringUtils.isBlank(original)

  private def baseParse(original: String): ValidatedNel[ExpressionCompilationError, Expression] = {
    Validated
      .catchNonFatal(parser.parseExpression(original, flavour.parserContext.orNull))
      .leftMap(ex => NonEmptyList.of(ExpressionCompilationError(ex.getMessage)))
  }

  private def expression(expression: ParsedSpelExpression, expectedType: TypingResult) = {
    if (enableSpelForceCompile) {
      forceCompile(expression.parsed)
    }
    new SpelExpression(expression, expectedType, flavour, prepareEvaluationContext)
  }

  def typingDictLabels =
    new SpelExpressionParser(
      parser,
      validator.withTyper(_.withDictTyper(new LabelsDictTyper(dictRegistry))),
      dictRegistry,
      enableSpelForceCompile,
      flavour,
      prepareEvaluationContext
    )

}

object SpelExpressionParser extends LazyLogging {

  sealed abstract class Flavour(val languageId: Language, val parserContext: Option[ParserContext])
  object Standard extends Flavour(GraphExpression.Language.Spel, None)
  // TODO: should we enable other prefixes/suffixes?
  object Template extends Flavour(GraphExpression.Language.SpelTemplate, Some(ParserContext.TEMPLATE_EXPRESSION))

  private[spel] final val LazyValuesProviderVariableName: String = "$lazy"
  private[spel] final val LazyContextVariableName: String        = "$lazyContext"

  // TODO
  // this does not work in every situation - e.g expression (#variable != '') is not compiled
  // maybe we could remove it altogether with "enableSpelForceCompile" flag after some investigation
  private[spel] def forceCompile(parsed: Expression): Unit = {
    parsed match {
      case e: standard.SpelExpression   => forceCompile(e)
      case e: CompositeStringExpression => e.getExpressions.foreach(forceCompile)
      case _: LiteralExpression         =>
      case _: NullExpression            =>
    }
  }

  private def forceCompile(spel: standard.SpelExpression): Unit = {
    val managedToCompile = spel.compileExpression()
    if (!managedToCompile) {
      spel.getAST match {
        case node: SpelNodeImpl if node.isCompilable =>
          throw new IllegalStateException(s"Failed to compile expression: ${spel.getExpressionString}")
        case _ => logger.debug(s"Expression ${spel.getExpressionString} will not be compiled")
      }
    } else {
      logger.debug(s"Compiled ${spel.getExpressionString} with compiler result: $spel")
    }
  }

  def default(
      classLoader: ClassLoader,
      expressionConfig: ExpressionConfigDefinition,
      dictRegistry: DictRegistry,
      enableSpelForceCompile: Boolean,
      flavour: Flavour,
      classDefinitionSet: ClassDefinitionSet
  ): SpelExpressionParser = {

    val parser = new org.springframework.expression.spel.standard.SpelExpressionParser(
      // we have to pass classloader, because default contextClassLoader can be sth different than we expect...
      new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, classLoader)
    )
    val evaluationContextPreparer = EvaluationContextPreparer.default(classLoader, expressionConfig, classDefinitionSet)
    val validator = new SpelExpressionValidator(
      Typer.default(classLoader, expressionConfig, new KeysDictTyper(dictRegistry), classDefinitionSet)
    )
    new SpelExpressionParser(
      parser,
      validator,
      dictRegistry,
      enableSpelForceCompile,
      flavour,
      evaluationContextPreparer
    )
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy