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

pl.touk.nussknacker.restmodel.validation.PrettyValidationErrors.scala Maven / Gradle / Ivy

There is a newer version: 1.18.1
Show newest version
package pl.touk.nussknacker.restmodel.validation

import org.apache.commons.lang3.StringUtils
import pl.touk.nussknacker.engine.api.context.ProcessCompilationError._
import pl.touk.nussknacker.engine.api.context.{ParameterValidationError, ProcessCompilationError}
import pl.touk.nussknacker.engine.api.generics.ExpressionParseError.ErrorDetails
import pl.touk.nussknacker.engine.api.parameter.ParameterName
import pl.touk.nussknacker.engine.api.util.ReflectUtils
import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess
import pl.touk.nussknacker.engine.graph.node._
import pl.touk.nussknacker.restmodel.validation.ValidationResults.{NodeValidationError, NodeValidationErrorType}

object PrettyValidationErrors {

  def formatErrorMessage(error: ProcessCompilationError): NodeValidationError = {
    val typ = getErrorTypeName(error)

    def node(
        message: String,
        description: String,
        errorType: NodeValidationErrorType.Value = NodeValidationErrorType.SaveAllowed,
        paramName: Option[ParameterName] = None,
        details: Option[ErrorDetails] = None
    ): NodeValidationError = NodeValidationError(typ, message, description, paramName.map(_.value), errorType, details)

    def handleParameterValidationError(error: ParameterValidationError): NodeValidationError =
      node(error.message, error.description, paramName = Some(error.paramName))

    error match {
      case ExpressionParserCompilationError(message, _, paramName, _, details) =>
        node(
          message = s"Failed to parse expression: $message",
          description =
            s"There is problem with expression in field ${paramName.map(_.value)} - it could not be parsed.",
          paramName = paramName,
          details = details
        )
      case FragmentParamClassLoadError(paramName, refClazzName, _) =>
        node(
          message = "Invalid parameter type.",
          description = s"Failed to load $refClazzName",
          paramName = Some(qualifiedParamFieldName(paramName = paramName, subFieldName = Some(TypFieldName)))
        )
      case DuplicatedNodeIds(ids) =>
        node(
          message = "Two nodes cannot have same id",
          description = s"Duplicate node ids: ${ids.mkString(", ")}",
          errorType = NodeValidationErrorType.RenderNotAllowed
        )
      case EmptyProcess       => node("Empty scenario", "Scenario is empty, please add some nodes")
      case InvalidRootNode(_) => node("Invalid root node", "Scenario can start only from source node")
      case InvalidTailOfBranch(_) =>
        node(
          "Scenario must end with a sink, processor or fragment",
          "Scenario must end with a sink, processor or fragment"
        )
      case error: IdError =>
        mapIdErrorToNodeError(error)
      case ScenarioNameValidationError(message, description) =>
        node(
          message = message,
          description = description,
          paramName = Some(ParameterName(CanonicalProcess.NameFieldName)),
        )
      case SpecificDataValidationError(field, message) => node(message, message, paramName = Some(field))
      case NonUniqueEdgeType(etype, nodeId) =>
        node(
          message = "Edges are not unique",
          description = s"Node $nodeId has duplicate outgoing edges of type: $etype, it cannot be saved properly",
          errorType = NodeValidationErrorType.SaveNotAllowed
        )
      case NonUniqueEdge(nodeId, target) =>
        node(
          message = "Edges are not unique",
          description = s"Node $nodeId has duplicate outgoing edges to: $target, it cannot be saved properly",
          errorType = NodeValidationErrorType.SaveNotAllowed
        )
      case LooseNode(nodeIds) =>
        val (message, description) = nodeIds.toList match {
          case nodeId :: Nil =>
            (
              "Loose node",
              s"Node $nodeId is not connected to source, it cannot be saved properly"
            )
          case _ =>
            (
              "Loose nodes",
              s"Nodes ${nodeIds.mkString(", ")} are not connected to source, it cannot be saved properly"
            )
        }
        node(
          message,
          description,
          errorType = NodeValidationErrorType.SaveNotAllowed
        )
      case DisabledNode(nodeId) =>
        node(
          message = s"Node $nodeId is disabled",
          description = "Deploying scenario with disabled node can have unexpected consequences",
          errorType = NodeValidationErrorType.SaveAllowed
        )

      case MissingParameters(params, _) =>
        node(
          message = s"Node parameters not filled: ${params.mkString(", ")}",
          description = s"Please fill missing node parameters: : ${params.mkString(", ")}"
        )

      case pve: ParameterValidationError => handleParameterValidationError(pve)

      // exceptions below should not really happen (unless services change and process becomes invalid)
      case MissingCustomNodeExecutor(id, _) =>
        node(s"Missing custom executor: $id", s"Please check the name of custom executor, $id is not available")
      case MissingService(id, _) =>
        node(s"Missing processor/enricher: $id", s"Please check the name of processor/enricher, $id is not available")
      case MissingSinkFactory(id, _) =>
        node(s"Missing sink: $id", s"Please check the name of sink, $id is not available")
      case MissingSourceFactory(id, _) =>
        node(s"Missing source: $id", s"Please check the name of source, $id is not available")
      case RedundantParameters(params, _) =>
        node(s"Redundant parameters", s"Please omit redundant parameters: ${params.mkString(", ")}")
      case WrongParameters(requiredParameters, passedParameters, _) =>
        node(
          message = s"Wrong parameters",
          description =
            s"Please provide ${requiredParameters.map(_.value).mkString(", ")} instead of ${passedParameters.map(_.value).mkString(", ")}"
        )
      case OverwrittenVariable(varName, _, paramName) =>
        node(
          message = s"Variable name '$varName' is already defined.",
          description = "You cannot overwrite variables",
          paramName = paramName
        )
      case InvalidVariableName(varName, _, paramName) =>
        node(
          message =
            s"Variable name '$varName' is not a valid identifier (only letters, numbers or '_', cannot be empty)",
          description = "Please use only letters, numbers or '_', also identifier cannot be empty.",
          paramName = paramName
        )
      case NotSupportedExpressionLanguage(languageId, _) =>
        node(s"Language $languageId is not supported", "Currently only SPEL expressions are supported")
      case MissingPart(id)             => node("MissingPart", s"Node $id has missing part")
      case UnsupportedPart(id)         => node("UnsupportedPart", s"Type of node $id is unsupported right now")
      case UnknownFragment(id, nodeId) => node("Unknown fragment", s"Node $nodeId uses fragment $id which is missing")
      case InvalidFragment(id, nodeId) => node("Invalid fragment", s"Node $nodeId uses fragment $id which is invalid")
      case FatalUnknownError(message) =>
        node("Unknown, fatal validation error", s"Fatal error: $message, please check configuration")
      case CannotCreateObjectError(message, nodeId) =>
        node(s"Could not create $nodeId: $message", s"Could not create $nodeId: $message")

      case UnresolvedFragment(id) => node("Unresolved fragment", s"fragment $id encountered, this should not happen")
      case FragmentOutputNotDefined(id, _) => node(s"Output $id not defined", "Please check fragment definition")
      case UnknownFragmentOutput(id, _)    => node(s"Unknown fragment output $id", "Please check fragment definition")
      case DisablingManyOutputsFragment(_) =>
        node(s"Cannot disable fragment with multiple outputs", "Please check fragment definition")
      case DisablingNoOutputsFragment(_) =>
        node(s"Cannot disable fragment with no outputs", "Please check fragment definition")
      case MissingRequiredProperty(paramName, label, _) => missingRequiredProperty(typ, paramName.value, label)
      case UnknownProperty(paramName, _)                => unknownProperty(typ, paramName.value)
      case InvalidPropertyFixedValue(paramName, label, value, values, _) =>
        invalidPropertyFixedValue(typ, paramName.value, label, value, values)
      case CustomNodeError(_, message, paramName) =>
        NodeValidationError(typ, message, message, paramName.map(_.value), NodeValidationErrorType.SaveAllowed, None)
      case e: DuplicateFragmentOutputNames =>
        node(
          message = s"Fragment output name '${e.duplicatedVarName}' has to be unique",
          description = "Please check fragment definition"
        )
      case DuplicateFragmentInputParameter(paramName, _) =>
        node(
          message = s"Parameter name '${paramName.value}' has to be unique",
          description = "Parameter name not unique",
          paramName = Some(qualifiedParamFieldName(paramName = paramName, subFieldName = Some(ParameterNameFieldName)))
        )
      case InitialValueNotPresentInPossibleValues(paramName, _) =>
        node(
          message =
            s"The initial value provided for parameter '${paramName.value}' is not present in the parameter's possible values list",
          description = "Please check component definition",
          paramName = Some(qualifiedParamFieldName(paramName = paramName, subFieldName = Some(InitialValueFieldName)))
        )
      case UnsupportedFixedValuesType(paramName, typ, _) =>
        node(
          message = s"Fixed values list can only be be provided for type String or Boolean, found: $typ",
          description = "Please check component definition",
          paramName = Some(qualifiedParamFieldName(paramName = paramName, subFieldName = Some(TypFieldName)))
        )
      case UnsupportedDictParameterEditorType(paramName, typ, _) =>
        node(
          s"Dictionary parameter editor can only be used for parameters of type String, Long or Boolean, found: $typ",
          "Please check component definition",
          paramName = Some(qualifiedParamFieldName(paramName = paramName, subFieldName = Some(TypFieldName)))
        )
      case RequireValueFromEmptyFixedList(paramName, _) =>
        node(
          s"Required parameter '${paramName.value}' cannot be a member of an empty fixed list",
          description = "Please check component definition",
          paramName = Some(qualifiedParamFieldName(paramName = paramName, subFieldName = Some(InputModeFieldName)))
        )
      case ExpressionParserCompilationErrorInFragmentDefinition(message, _, paramName, subFieldName, originalExpr) =>
        node(
          message = s"Failed to parse expression: $message",
          description = s"There is a problem with expression: $originalExpr",
          paramName = Some(qualifiedParamFieldName(paramName = paramName, subFieldName = subFieldName))
        )
      case InvalidValidationExpression(message, _, paramName, originalExpr) =>
        node(
          s"Invalid validation expression: $message",
          description = s"There is a problem with validation expression: $originalExpr",
          paramName =
            Some(qualifiedParamFieldName(paramName = paramName, subFieldName = Some(ValidationExpressionFieldName)))
        )
      case EmptyMandatoryField(_, qualifiedFieldName) =>
        node(
          message = s"This field is mandatory and cannot be empty",
          description = s"This field is mandatory and cannot be empty",
          paramName = Some(qualifiedFieldName)
        )
      case DictNotDeclared(dictId, _, paramName) =>
        node(
          message = s"Dictionary not declared: $dictId",
          description = s"Dictionary not declared: $dictId",
          paramName = Some(paramName)
        )
      case DictIsOfInvalidType(dictId, actualType, expectedType, _, paramName) =>
        node(
          s"Dictionary '$dictId' contains values of invalid type",
          s"Values in dictionary '$dictId' are of type '${actualType.display}' and cannot be treated as expected type: '${expectedType.display}'",
          paramName = Some(paramName)
        )
      case DictEntryWithKeyNotExists(dictId, key, possibleKeys, _, paramName) =>
        node(
          s"Dictionary $dictId doesn't contain entry with key: $key",
          description = s"Dictionary $dictId possible keys: $possibleKeys",
          paramName = Some(paramName)
        )
      case DictEntryWithLabelNotExists(dictId, label, possibleLabels, _, paramName) =>
        node(
          message = s"Dictionary $dictId doesn't contain entry with label: $label",
          description = s"Dictionary $dictId possible labels: $possibleLabels",
          paramName = Some(paramName)
        )
      case DictLabelByKeyResolutionFailed(dictId, key, _, paramName) =>
        node(
          s"Failed to resolve label for key: $key in dict: $dictId",
          description = s"Dict registry doesn't support fetching of label for dictId: $dictId",
          paramName = Some(paramName)
        )
      case KeyWithLabelExpressionParsingError(keyWithLabel, message, paramName, _) =>
        node(
          message = s"Error while parsing KeyWithLabel expression: $keyWithLabel",
          description = message,
          paramName = Some(paramName)
        )
    }
  }

  private def unknownProperty(typ: String, propertyName: String): NodeValidationError =
    NodeValidationError(
      typ = typ,
      message = s"Unknown property $propertyName",
      description = s"Property $propertyName is not known",
      fieldName = Some(propertyName),
      errorType = NodeValidationErrorType.SaveAllowed,
      details = None
    )

  private def missingRequiredProperty(typ: String, fieldName: String, label: Option[String]) = {
    val labelText = getLabel(label)
    NodeValidationError(
      typ = typ,
      message = s"Configured property $fieldName$labelText is missing",
      description = s"Please fill missing property $fieldName$labelText",
      fieldName = Some(fieldName),
      errorType = NodeValidationErrorType.SaveAllowed,
      details = None
    )
  }

  private def invalidPropertyFixedValue(
      typ: String,
      propertyName: String,
      label: Option[String],
      value: String,
      values: List[String]
  ) = {
    val labelText = getLabel(label)
    NodeValidationError(
      typ = typ,
      message = s"Property $propertyName$labelText has invalid value",
      description = s"Expected one of ${values.mkString(", ")}, got: $value.",
      fieldName = Some(propertyName),
      errorType = NodeValidationErrorType.SaveAllowed,
      details = None
    )
  }

  private def getLabel(label: Option[String]) = label match {
    case Some(text) => s" ($text)"
    case None       => StringUtils.EMPTY
  }

  private def getErrorTypeName(error: ProcessCompilationError) = ReflectUtils.simpleNameWithoutSuffix(error.getClass)

  private def mapIdErrorToNodeError(error: IdError) = {
    val validatedObjectType = error match {
      case ScenarioNameError(_, _, isFragment) => if (isFragment) "Fragment" else "Scenario"
      case NodeIdValidationError(_, _)         => "Node"
    }
    val errorSeverity = error match {
      case ScenarioNameError(_, _, _) => NodeValidationErrorType.SaveAllowed
      case NodeIdValidationError(errorType, _) =>
        errorType match {
          case ProcessCompilationError.EmptyValue | IllegalCharactersId(_) => NodeValidationErrorType.RenderNotAllowed
          case _                                                           => NodeValidationErrorType.SaveAllowed
        }
    }
    val fieldName = error match {
      case ScenarioNameError(_, _, _)  => CanonicalProcess.NameFieldName
      case NodeIdValidationError(_, _) => pl.touk.nussknacker.engine.graph.node.IdFieldName
    }
    val message = error.errorType match {
      case ProcessCompilationError.EmptyValue       => s"$validatedObjectType name is mandatory and cannot be empty"
      case ProcessCompilationError.BlankId          => s"$validatedObjectType name cannot be blank"
      case ProcessCompilationError.LeadingSpacesId  => s"$validatedObjectType name cannot have leading spaces"
      case ProcessCompilationError.TrailingSpacesId => s"$validatedObjectType name cannot have trailing spaces"
      case ProcessCompilationError.IllegalCharactersId(illegalCharactersHumanReadable) =>
        s"$validatedObjectType name contains invalid characters. $illegalCharactersHumanReadable are not allowed"
    }
    val description = error.errorType match {
      case ProcessCompilationError.EmptyValue      => s"Empty ${validatedObjectType.toLowerCase} name"
      case ProcessCompilationError.BlankId         => s"Blank ${validatedObjectType.toLowerCase} name"
      case ProcessCompilationError.LeadingSpacesId => s"Leading spaces in ${validatedObjectType.toLowerCase} name"
      case ProcessCompilationError.TrailingSpacesId =>
        s"Trailing spaces in ${validatedObjectType.toLowerCase} name"
      case ProcessCompilationError.IllegalCharactersId(_) =>
        s"Invalid characters in ${validatedObjectType.toLowerCase} name"
    }
    NodeValidationError(
      typ = getErrorTypeName(error),
      message = message,
      description = description,
      fieldName = Some(fieldName),
      errorType = errorSeverity,
      details = None
    )
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy