If you want to restrict the input to a limited language, you can easily create a parser for that language using only the Scala core library.
I have done this for a stripped down version of your example
object WeekDay extends Enumeration {
type WeekDay = Value; val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
}
case class ExampleObject(val integerValue1 : Integer, val stringValue1 : String, val weekDay: WeekDay.Value){
def intReturningMethod1()= {0}
}
First I use an import and create some helpers:
type FilterCriterion = ExampleObject => Boolean
type Extractor[T] = ExampleObject => T
def compare[T <% Ordered[T]](v1 : T, c : String, v2 : T) : Boolean = c match {
case "<" => v1 < v2
case ">" => v1 > v2
case "==" => v1 == v2
}
def compareAny(v1: Any, c : String, v2 : Any) : Boolean = (v1,v2) match {
case (s1: String, s2:String) => compare(s1,c,s2)
case (i1: Int, i2 : Int) => compare(i1,c,i2)
case (w1 : WeekDay.WeekDay, w2 : WeekDay.WeekDay) => compare(w1.id, c, w2.id)
case _ => throw new IllegalArgumentException(s"Cannot compare ${v1.getClass} with ${v2.getClass}")
}
Then I create the parser:
object FilterParser extends JavaTokenParsers {
def intExtractor : Parser[Extractor[Int]] = wholeNumber ^^ {s => Function.const(s.toInt)_} |
"intReturningMethod1()" ^^^ {(e : ExampleObject) => e.intReturningMethod1()} |
"integerValue1" ^^^ {_.integerValue1}
def stringExtractor : Parser[Extractor[String]] = stringLiteral ^^ {s => Function.const(s.drop(1).dropRight(1))_} |
"stringValue1" ^^^ {_.stringValue1}
def weekDayExtrator : Parser[Extractor[WeekDay.WeekDay]] = stringLiteral ^? {
case s if WeekDay.values.exists(_.toString == s) => Function.const(WeekDay.withName(s))_
}
def extractor : Parser[Extractor[Any]] = intExtractor | stringExtractor | weekDayExtrator
def compareOp : Parser[FilterCriterion] = (extractor ~ ("<"| "==" | ">") ~ extractor) ^^ {
case v1 ~ c ~ v2 => (e : ExampleObject) => compareAny(v1(e),c,v2(e))
}
def simpleExpression : Parser[FilterCriterion] = "(" ~> expression <~ ")" | compareOp
def notExpression : Parser[FilterCriterion] = "!" ~> simpleExpression ^^ {(ex) => (e : ExampleObject) => !ex(e)} |
simpleExpression
def andExpression : Parser[FilterCriterion] = repsep(notExpression,"&&") ^^ {(exs) => (e : ExampleObject) => exs.foldLeft(true)((b,ex)=> b && ex(e))}
def orExpression : Parser[FilterCriterion] = repsep(andExpression,"||") ^^ {(exs) => (e : ExampleObject) => exs.foldLeft(false)((b,ex)=> b || ex(e))}
def expression : Parser[FilterCriterion] = orExpression
def parseExpressionString(s : String) = parseAll(expression, s)
}
This parser takes your input string and returns a function that maps an ExampleObject to a boolean value. This test function is constructed once while parsing the input string, using the pre-defined helper functions and the anonymous functions defined in the parser rules. The interpretation of the input string is only done once, while constructing the test function. When you execute the test function, you will run compiled Scale code. So it should run quite fast.
The test function is safe, because it does not allow the user to run arbitrary Scala code. It will just be constructed from the partial function provided in the parser and the pre-defined helpers.
val parsedCriterion=FilterParser.parseExpressionString("""((integerValue1 > 100 || integerValue1 < 50) && (stringValue1 == "A"))""")
List(ExampleObject(1,"A", WeekDay.Mon), ExampleObject(2,"B", WeekDay.Sat), ExampleObject(50,"A", WeekDay.Mon)).filter(parsedCriterion.get)
You can easily extend the parser yourself, when you want it to use more functions or more fields in your ExampleObject.