How to check if a variable is defined in Pebble Templates?

2.5k Views Asked by At

Using Pebble version 3.0.6.

I need to check if value 'v' has a specific variable (translated to Java: if Object v has a specific property). Looking for something like

{% if v instanceof test.MyClass %}
   ...
{% endif %}

or

{% if v has myProperty %}
   ...
{% endif %}

Both are not available as far as i know. What is the best way to achieve this with Pebble?

UPDATE

Context:

  • Using strictVariables = true
  • property is not a boolean, String or Number
1

There are 1 best solutions below

0
On BEST ANSWER

Not a builtin one but pebble allows you to write custom extensions. In java instanceof is an operator, which is something pebble allows you to write an extension for.

We need 3 things to write a custom extension for an operator:

  1. A class which describes the operator (implements BinaryOperator)
  2. A class which describes how the operator is evaluated (extends BinaryExpression<Object>)
  3. A class which adds this operator to the binary operators of pebble, this is the extension class and should implements Extension.

Step 1

We define the operator as instanceof with a precedence of 30, according to java the precedence of instanceof is the same as < > <= >=, in pebble these operators have a precedence of 30 so we use that. The node which evaluates this operation is InstanceofExpression.class, which is the class we will create in step 2.

   public class InstanceofOperator implements BinaryOperator {

    /**
     * This precedence is set based on
     * <a href="https://docs.oracle.com/javase/tutorial/java/nutsandbolts/operators.html">Java
     * Operators</a> 30 is the same precedence pebble has set for operators like {@code instanceof}
     * like <a href="https://github.com/PebbleTemplates/pebble/wiki/extending-pebble">Extending
     * Pebble</a>.
     */
    public int getPrecedence() {
        return 30;
    }

    public String getSymbol() {
        return "instanceof";
    }

    public Class<? extends BinaryExpression<?>> getNodeClass() {
        return InstanceofExpression.class;
    }

    public Associativity getAssociativity() {
        return Associativity.LEFT;
    }

}

Step 2

We now must write what the operator evaluates to, in this case we will return true if left instanceof right. For the right part of this evaluation we use a String which must contain the full qualifying name for the class, for example 1 instanceof "java.lang.String" which will return false, or 1 instanceof "java.lang.Long" which will return true.

An exception will be thrown if the right class cannot be found/loaded with Class.forName.

public class InstanceofExpression extends BinaryExpression<Object> {

    @Override
    public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) {
        // The left class (left instanceof right)
        Object leftClass = getLeftExpression().evaluate(self, context);

        // The right class, this is a string with the full qualifying name of the class eg (left
        // instanceof "java.lang.String")
        String rightClassname = getRightExpression().evaluate(self, context).toString();

        // We must get the right class as Class<?> in order to check if left is an instanceof right
        Class<?> rightClass;
        try {
            rightClass = Class.forName(rightClassname);
        } catch (ClassNotFoundException e) {
            throw new PebbleException(e.getCause(),
                    String.format("Cannot find class %s", rightClassname));
        }

        // Check if the left class is an instanceof the right class
        return rightClass.isInstance(leftClass);
    }

}

Step 3

We now must create an extension for Pebble, this is quite simple. We create an instanceof our custom InstanceofOperator and return that as binary operator:

public class InstanceofExtension implements Extension {

    @Override
    public List<BinaryOperator> getBinaryOperators() {
        return Arrays.asList(new InstanceofOperator());
    }

    // ...
    // Other methods required by implementing Extension, these other methods can just return null.
    // ...
    // ...

}

Alternatively instead of the entire Step 1 you can implement the getBinaryOperators method as such:

@Override
public List<BinaryOperator> getBinaryOperators() {
  return Arrays.asList(new BinaryOperatorImpl("instanceof", 30, InstanceofExpression.class,
            Associativity.LEFT));
}

Profit!

We can now add our custom extension with .extension(new InstanceofExtension()):

PebbleEngine engine =
        new PebbleEngine.Builder().strictVariables(true)
                .extension(new InstanceofExtension()).build();

PebbleTemplate compiledTemplate = engine.getTemplate("home.html");

// Test with Person as v
Writer personWriter = new StringWriter();

Map<String, Object> context = new HashMap<>();
context.put("v", new Person());
compiledTemplate.evaluate(personWriter, context);

System.out.println(personWriter.toString()); // <b>asdasdasdasds</b> is present

// Test with Fruit as v
Writer fruitWriter = new StringWriter();

context.put("v", new Fruit());
compiledTemplate.evaluate(fruitWriter, context);

System.out.println(fruitWriter.toString()); // <b>asdasdasdasds</b> is not present, but
                                            // <b>red</b> is

The Person class which we are processing above is defined as such that it extends Entity. To prove the concept works we also have a class Fruit which does not extend Entity. We test both of these different classes in v:

class Person extends Entity {
    public String name = "me";

}

class Entity {
    public String asd = "asdasdasdasds";

}

class Fruit {
    public String color = "red";
}

home.html we check if v which is Person or Fruit is an instance of com.mypackage.test.Entity or com.mypackage.test.Fruit:

<html>
    <body>
        {% if v instanceof "com.mypackage.test.Entity" %}
            <b>{{ v.asd }}</b>
        {% endif %}
        {% if v instanceof "com.mypackage.test.Fruit" %}
            <b>{{ v.color }}</b>
        {% endif %}
    </body>
</html>

The output is:

<html>
    <body>
        <b>asdasdasdasds</b>
    </body>
</html>
<html>
    <body>
        <b>red</b>
    </body>
</html>

Comments

The "left not instanceof right" version is:

{% if not (v instanceof "com.mypackage.test.entity") %}