Repository: PebbleTemplates/pebble
Pull request #453 (2019-07-13 17:16:24)

Allows variables to be set in templates when provided context is an immutable map


Patch:
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java b/pebble/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java
index 373d0c32..a93458c0 100644
--- a/pebble/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/template/PebbleTemplateImpl.java
@@ -89,12 +89,18 @@ public class PebbleTemplateImpl implements PebbleTemplate {
   public void evaluate(Writer writer, Map<String, Object> map) throws IOException {
     EvaluationContextImpl context = this.initContext(null);
     context.getScopeChain().pushScope(map);
+
+    // Issue #449: if the provided map is immutable, this allows us to still set variables in the template context
+    context.getScopeChain().pushScope(new HashMap<>());
     this.evaluate(writer, context);
   }
 
   public void evaluate(Writer writer, Map<String, Object> map, Locale locale) throws IOException {
     EvaluationContextImpl context = this.initContext(locale);
     context.getScopeChain().pushScope(map);
+
+    // Issue #449: if the provided map is immutable, this allows us to still set variables in the template context
+    context.getScopeChain().pushScope(new HashMap<>());
     this.evaluate(writer, context);
   }
 


Test patch:
diff --git a/pebble/src/test/java/com/mitchellbosecke/pebble/ContextTest.java b/pebble/src/test/java/com/mitchellbosecke/pebble/ContextTest.java
index f9401f0e..a1fbffcb 100644
--- a/pebble/src/test/java/com/mitchellbosecke/pebble/ContextTest.java
+++ b/pebble/src/test/java/com/mitchellbosecke/pebble/ContextTest.java
@@ -8,19 +8,25 @@
  */
 package com.mitchellbosecke.pebble;
 
-import static org.junit.Assert.assertEquals;
-
 import com.mitchellbosecke.pebble.error.PebbleException;
 import com.mitchellbosecke.pebble.error.RootAttributeNotFoundException;
 import com.mitchellbosecke.pebble.loader.StringLoader;
 import com.mitchellbosecke.pebble.template.PebbleTemplate;
+import org.junit.Test;
+
 import java.io.IOException;
 import java.io.StringWriter;
 import java.io.Writer;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
-import org.junit.Test;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import static org.junit.Assert.assertEquals;
 
 public class ContextTest {
 
@@ -129,4 +135,168 @@ public class ContextTest {
     template.evaluate(writer);
     assertEquals("{{ template.name }}", writer.toString());
   }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testImmutableMapThrows() throws PebbleException, IOException {
+    Map<String, Object> originalMap = new HashMap<>();
+    originalMap.put("contextVariable", "context variable value");
+    Map<String, Object> immutableMap = new ImmutableMap<>(originalMap);
+
+    immutableMap.put("templateVariable", "template variable value");
+  }
+
+  @Test
+  public void testImmutableContext() throws PebbleException, IOException {
+    PebbleEngine pebble = new PebbleEngine
+            .Builder()
+            .loader(new StringLoader())
+            .strictVariables(false)
+            .build();
+    PebbleTemplate template = pebble.getTemplate("" +
+            "{% set templateVariable = 'template variable value' %}" +
+            "{{ contextVariable }} - {{ templateVariable }}");
+
+    Writer writer = new StringWriter();
+
+    Map<String, Object> originalMap = new HashMap<>();
+    originalMap.put("contextVariable", "context variable value");
+
+    template.evaluate(writer, new ImmutableMap<>(originalMap));
+    assertEquals("context variable value - template variable value", writer.toString());
+  }
+
+  private static class ImmutableMap<T, U> implements Map<T, U> {
+
+    private final Map<T, U> delegate;
+
+    private ImmutableMap(Map<T, U> delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public int size() {
+      return delegate.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return delegate.isEmpty();
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+      return delegate.containsKey(key);
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+      return delegate.containsValue(value);
+    }
+
+    @Override
+    public U get(Object key) {
+      return delegate.get(key);
+    }
+
+    @Override
+    public U put(T key, U value) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public U remove(Object key) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public void putAll(Map<? extends T, ? extends U> m) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public void clear() {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public Set<T> keySet() {
+      return delegate.keySet();
+    }
+
+    @Override
+    public Collection<U> values() {
+      return delegate.values();
+    }
+
+    @Override
+    public Set<Entry<T, U>> entrySet() {
+      return delegate.entrySet();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return delegate.equals(o);
+    }
+
+    @Override
+    public int hashCode() {
+      return delegate.hashCode();
+    }
+
+    @Override
+    public U getOrDefault(Object key, U defaultValue) {
+      return delegate.getOrDefault(key, defaultValue);
+    }
+
+    @Override
+    public void forEach(BiConsumer<? super T, ? super U> action) {
+      delegate.forEach(action);
+    }
+
+    @Override
+    public void replaceAll(BiFunction<? super T, ? super U, ? extends U> function) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public U putIfAbsent(T key, U value) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public boolean remove(Object key, Object value) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public boolean replace(T key, U oldValue, U newValue) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public U replace(T key, U value) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public U computeIfAbsent(T key, Function<? super T, ? extends U> mappingFunction) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public U computeIfPresent(T key, BiFunction<? super T, ? super U, ? extends U> remappingFunction) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public U compute(T key, BiFunction<? super T, ? super U, ? extends U> remappingFunction) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+
+    @Override
+    public U merge(T key, U value, BiFunction<? super U, ? super U, ? extends U> remappingFunction) {
+      throw new UnsupportedOperationException("this map is immutable");
+    }
+  }
+
 }