HubSpot / jinjava

Jinja template engine for Java
Apache License 2.0
690 stars 168 forks source link

Rendering template with custom function in Scala throws NullPointerException #1171

Closed tuzonghua closed 5 months ago

tuzonghua commented 5 months ago

I'm trying to render a simple custom function in a Jinja template and getting a NullPointerException. I've included the elided stack trace. My example below is in Scala, hopefully that's not an issue unto itself.

Build env:

Scala 2.13.10
Java 11.0.20
Jinjava 2.7.2

Sample code:

import com.hubspot.jinjava.Jinjava
import com.hubspot.jinjava.lib.fn.ELFunctionDefinition

import java.util

class MyFuncsClass {
  def isDayOfWeek(param: String): Boolean =
    true
}

val jinjava: Jinjava = new Jinjava()
val context: util.HashMap[String, Object] = new util.HashMap[String, Object]()
val template: String = "{% if is_day_of_week(input_date) %} SELECT {{ input_date }} FROM `table` {% endif %}"

jinjava.getGlobalContext.registerFunction(
  new ELFunctionDefinition(
    "",
    "is_day_of_week",
    classOf[MyFuncsClass],
    "isDayOfWeek",
    classOf[String]
  )
)

context.put("input_date", "2024-03-19")
val renderedTemplate: String = jinjava.render(template, context)

println(renderedTemplate)

Output:

com.hubspot.jinjava.interpret.FatalTemplateErrorsException: InterpretException: Error resolving expression [is_day_of_week(input_date)]: NullPointerException:
  at com.hubspot.jinjava.Jinjava.render(Jinjava.java:193)
  ... 40 elided
tuzonghua commented 5 months ago

It looks like this doesn't even work in Java. Not sure what I'm missing. There aren't a ton of examples out there to compare to.

import com.google.common.collect.Maps;
import com.hubspot.jinjava.Jinjava;
import com.hubspot.jinjava.lib.fn.ELFunctionDefinition;

import java.util.Map;

public class JinjavaTest {
    public static void main(String[] args) {
        Jinjava jinjava = new Jinjava();
        Map<String, Object> context = Maps.newHashMap();
        String template = "{% if is_day_of_week(input_date) %} SELECT {{ input_date }} FROM `table` {% endif %}";
        jinjava.getGlobalContext().registerFunction(
                new ELFunctionDefinition(
                        "",
                        "is_day_of_week",
                        JinjavaTest.class,
                        "isDayOfWeek",
                        String.class
                )
        );
        context.put("input_date", "2024-03-23");
        String renderedTemplate = jinjava.render(template, context);
        System.out.println(renderedTemplate);
    }

    boolean isDayOfWeek(String param) {
        return true;
    }
}
tuzonghua commented 5 months ago

It turned out to be my mistake. My Java example above was incorrectly declaring the isDayOfWeek method (it should be public static boolean isDayOfWeek()). As for the Scala code, the example above compiles to public boolean isDayOfWeek(java.lang.String); rather than public static boolean isDayOfWeek(java.lang.String);. The correct approach is to use a companion object to compile to the correct Java method signature, like so:

class MyFuncsClass {
  import MyFuncsClass.isDayOfWeek
}

object MyFuncsClass {
  def isDayOfWeek(param: String): Boolean =
    true
}
jasmith-hs commented 5 months ago

@tuzonghua Check out https://github.com/HubSpot/jinjava/blob/master/src/test/java/com/hubspot/jinjava/lib/fn/InjectedContextFunctionProxyTest.java#L73 for if you want to register non-static methods

tuzonghua commented 5 months ago

@jasmith-hs Thanks! I tried this and got a different error. I think that it's still due to what the Scala code gets compiled to (the class containing the target method gets compiled to public class MyFuncsClass rather than public static class MyFuncsClass). I'm fine using Scala companion objects for now.

package some.test.package

import com.hubspot.jinjava.Jinjava
import com.hubspot.jinjava.lib.fn.{ELFunctionDefinition, InjectedContextFunctionProxy}

import java.lang.reflect.Method
import java.util

class MyFuncsClass {
  def isDayOfWeek(param: String): Boolean =
    true
}

object JinjavaScalaTest extends App {
  val jinjava: Jinjava                      = new Jinjava()
  val context: util.HashMap[String, Object] = new util.HashMap[String, Object]()
  val template: String                      = "{% if is_day_of_week(input_date) %} SELECT {{ input_date }} FROM `table` {% endif %}"
  val m: Method                             = classOf[MyFuncsClass].getDeclaredMethod("isDayOfWeek", classOf[String])
  val instance                              = new MyFuncsClass

  val proxy: ELFunctionDefinition = InjectedContextFunctionProxy.defineProxy(
    "",
    "is_day_of_week",
    m,
    instance
  )

  jinjava.getGlobalContext.registerFunction(proxy)
  context.put("input_date", "2024-03-19")
  val renderedTemplate: String = jinjava.render(template, context)

  println(renderedTemplate)
}

Output:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.reflect.AccessibleObject.setAccessible(boolean)" because "ao" is null
    at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:103)
    at javassist.util.proxy.DefineClassHelper.toClass3(DefineClassHelper.java:151)
    at javassist.util.proxy.DefineClassHelper.toClass2(DefineClassHelper.java:134)
    at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:95)
    at javassist.ClassPool.toClass(ClassPool.java:1143)
    at javassist.ClassPool.toClass(ClassPool.java:1106)
    at javassist.ClassPool.toClass(ClassPool.java:1064)
    at javassist.CtClass.toClass(CtClass.java:1275)
    at com.hubspot.jinjava.lib.fn.InjectedContextFunctionProxy.defineProxy(InjectedContextFunctionProxy.java:82)
    at some.test.package.JinjavaScalaTest$.delayedEndpoint$some$test$package$JinjavaScalaTest$1(JinjavaScalaTest.scala:25)
    at some.test.package.JinjavaScalaTest$delayedInit$body.apply(JinjavaScalaTest.scala:14)
    at scala.Function0.apply$mcV$sp(Function0.scala:42)
    at scala.Function0.apply$mcV$sp$(Function0.scala:42)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17)
    at scala.App.$anonfun$main$1(App.scala:98)
    at scala.App.$anonfun$main$1$adapted(App.scala:98)
    at scala.collection.IterableOnceOps.foreach(IterableOnce.scala:575)
    at scala.collection.IterableOnceOps.foreach$(IterableOnce.scala:573)
    at scala.collection.AbstractIterable.foreach(Iterable.scala:933)
    at scala.App.main(App.scala:98)
    at scala.App.main$(App.scala:96)
    at some.test.package.JinjavaScalaTest$.main(JinjavaScalaTest.scala:14)
    at some.test.package.JinjavaScalaTest.main(JinjavaScalaTest.scala)