Repository: PebbleTemplates/pebble
Pull request #281 (2017-06-19 16:14:22)

make attribute resolving extendable (branch 3.0)
.. need to extend attribute resolving (if i can not add custom interfaces and it is not a map, a list or an array..)


Patch:
diff --git a/pom.xml b/pom.xml
index 737260b4..736b3770 100644
--- a/pom.xml
+++ b/pom.xml
@@ -252,4 +252,4 @@
         <url>http://github.com/mbosecke/pebble.git</url>
         <tag>HEAD</tag>
     </scm>
-</project>
+</project>
\ No newline at end of file
diff --git a/src/main/java/com/mitchellbosecke/pebble/PebbleEngine.java b/src/main/java/com/mitchellbosecke/pebble/PebbleEngine.java
index 8769071d..b117aa3a 100644
--- a/src/main/java/com/mitchellbosecke/pebble/PebbleEngine.java
+++ b/src/main/java/com/mitchellbosecke/pebble/PebbleEngine.java
@@ -17,6 +17,7 @@ import com.mitchellbosecke.pebble.error.ParserException;
 import com.mitchellbosecke.pebble.extension.Extension;
 import com.mitchellbosecke.pebble.extension.ExtensionRegistry;
 import com.mitchellbosecke.pebble.extension.NodeVisitorFactory;
+import com.mitchellbosecke.pebble.extension.core.AttributeResolverExtension;
 import com.mitchellbosecke.pebble.extension.core.CoreExtension;
 import com.mitchellbosecke.pebble.extension.escaper.EscaperExtension;
 import com.mitchellbosecke.pebble.extension.escaper.EscapingStrategy;
@@ -446,12 +447,13 @@ public class PebbleEngine {
          */
         public PebbleEngine build() {
 
-            // core extensions
-            List<Extension> extensions = new ArrayList<>();
-            extensions.add(new CoreExtension());
+          // core extensions
+          List<Extension> extensions = new ArrayList<>();
+          extensions.add(new CoreExtension());
           extensions.add(this.escaperExtension);
-            extensions.add(new I18nExtension());
-            extensions.addAll(this.userProvidedExtensions);
+          extensions.add(new I18nExtension());
+          extensions.addAll(this.userProvidedExtensions);
+          extensions.add(new AttributeResolverExtension());
 
             // default loader
           if (this.loader == null) {
diff --git a/src/main/java/com/mitchellbosecke/pebble/attributes/ArrayResolver.java b/src/main/java/com/mitchellbosecke/pebble/attributes/ArrayResolver.java
new file mode 100644
index 00000000..381a0cb1
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/attributes/ArrayResolver.java
@@ -0,0 +1,48 @@
+package com.mitchellbosecke.pebble.attributes;
+
+import com.mitchellbosecke.pebble.error.AttributeNotFoundException;
+
+import java.lang.reflect.Array;
+import java.util.Optional;
+
+import static java.util.Optional.empty;
+
+public class ArrayResolver implements AttributeResolver {
+
+  @Override
+  public Optional<ResolvedAttribute> resolve(Object instance,
+                                             Object attribute,
+                                             Object[] argumentValues,
+                                             boolean isStrictVariables,
+                                             String filename,
+                                             int lineNumber) {
+    if (argumentValues == null && instance.getClass().isArray()) {
+      String attributeName = String.valueOf(attribute);
+      Optional<Integer> optIndex = asIndex(attributeName);
+      if (optIndex.isPresent()) {
+        int index = optIndex.get();
+        int length = Array.getLength(instance);
+        if (index < 0 || index >= length) {
+          if (isStrictVariables) {
+            throw new AttributeNotFoundException(null,
+                "Index out of bounds while accessing array with strict variables on.",
+                attributeName, lineNumber, filename);
+          } else {
+            return Optional.of(() -> null);
+          }
+        }
+        return Optional.of(() -> Array.get(instance, index));
+      }
+    }
+    return empty();
+  }
+
+  public static Optional<Integer> asIndex(String attributeName) {
+    try {
+      return Optional.of(Integer.parseInt(attributeName));
+    } catch (NumberFormatException nx) {
+
+    }
+    return empty();
+  }
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/attributes/AttributeResolver.java b/src/main/java/com/mitchellbosecke/pebble/attributes/AttributeResolver.java
new file mode 100644
index 00000000..1ad47859
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/attributes/AttributeResolver.java
@@ -0,0 +1,12 @@
+package com.mitchellbosecke.pebble.attributes;
+
+import java.util.Optional;
+
+public interface AttributeResolver {
+  Optional<ResolvedAttribute> resolve(Object instance,
+                                      Object attribute,
+                                      Object[] argumentValues,
+                                      boolean isStrictVariables,
+                                      String filename,
+                                      int lineNumber);
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/attributes/DefaultAttributeResolver.java b/src/main/java/com/mitchellbosecke/pebble/attributes/DefaultAttributeResolver.java
new file mode 100644
index 00000000..31c298e2
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/attributes/DefaultAttributeResolver.java
@@ -0,0 +1,34 @@
+package com.mitchellbosecke.pebble.attributes;
+
+import java.util.List;
+import java.util.Optional;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.unmodifiableList;
+import static java.util.Optional.empty;
+
+public class DefaultAttributeResolver implements AttributeResolver {
+  private final List<AttributeResolver> resolvers = unmodifiableList(asList(
+      new MapResolver(),
+      new ArrayResolver(),
+      new ListResolver(),
+      new DynamicAttributeProviderResolver(),
+      new MemberResolver()));
+
+  @Override
+  public Optional<ResolvedAttribute> resolve(Object instance,
+                                             Object attribute,
+                                             Object[] argumentValues,
+                                             boolean isStrictVariables,
+                                             String filename,
+                                             int lineNumber) {
+    if (instance != null) {
+      return this.resolvers.stream()
+              .map(resolver -> resolver.resolve(instance, attribute, argumentValues, isStrictVariables, filename, lineNumber))
+              .filter(Optional::isPresent)
+              .findFirst()
+              .orElse(empty());
+    }
+    return empty();
+  }
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/attributes/DynamicAttributeProviderResolver.java b/src/main/java/com/mitchellbosecke/pebble/attributes/DynamicAttributeProviderResolver.java
new file mode 100644
index 00000000..cceaf5a2
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/attributes/DynamicAttributeProviderResolver.java
@@ -0,0 +1,26 @@
+package com.mitchellbosecke.pebble.attributes;
+
+import com.mitchellbosecke.pebble.extension.DynamicAttributeProvider;
+
+import java.util.Optional;
+
+public class DynamicAttributeProviderResolver implements AttributeResolver {
+
+  @Override
+  public Optional<ResolvedAttribute> resolve(Object instance,
+                                             Object attribute,
+                                             Object[] argumentValues,
+                                             boolean isStrictVariables,
+                                             String filename,
+                                             int lineNumber) {
+    if (instance instanceof DynamicAttributeProvider) {
+      DynamicAttributeProvider dynamicAttributeProvider = (DynamicAttributeProvider) instance;
+      if (dynamicAttributeProvider.canProvideDynamicAttribute(attribute)) {
+        return Optional.of(() -> dynamicAttributeProvider.getDynamicAttribute(attribute, argumentValues));
+      }
+    }
+
+    return Optional.empty();
+  }
+
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/attributes/ListResolver.java b/src/main/java/com/mitchellbosecke/pebble/attributes/ListResolver.java
new file mode 100644
index 00000000..629b8c53
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/attributes/ListResolver.java
@@ -0,0 +1,45 @@
+package com.mitchellbosecke.pebble.attributes;
+
+import com.mitchellbosecke.pebble.error.AttributeNotFoundException;
+
+import java.util.List;
+import java.util.Optional;
+
+import static java.util.Optional.empty;
+
+public class ListResolver implements AttributeResolver {
+
+  @Override
+  public Optional<ResolvedAttribute> resolve(Object instance,
+                                             Object attribute,
+                                             Object[] argumentValues,
+                                             boolean isStrictVariables,
+                                             String filename,
+                                             int lineNumber) {
+    if (argumentValues == null && instance instanceof List) {
+      String attributeName = String.valueOf(attribute);
+
+      @SuppressWarnings("unchecked") List<Object> list = (List<Object>) instance;
+
+      Optional<Integer> optIndex = ArrayResolver.asIndex(attributeName);
+      if (optIndex.isPresent()) {
+        int index = optIndex.get();
+        int length = list.size();
+
+        if (index < 0 || index >= length) {
+          if (isStrictVariables) {
+            throw new AttributeNotFoundException(null,
+                "Index out of bounds while accessing array with strict variables on.",
+                attributeName, lineNumber, filename);
+          } else {
+            return Optional.of(() -> null);
+          }
+        }
+
+        return Optional.of(() -> list.get(index));
+      }
+    }
+    return empty();
+  }
+
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/attributes/MapResolver.java b/src/main/java/com/mitchellbosecke/pebble/attributes/MapResolver.java
new file mode 100644
index 00000000..dca0e02e
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/attributes/MapResolver.java
@@ -0,0 +1,59 @@
+package com.mitchellbosecke.pebble.attributes;
+
+import com.mitchellbosecke.pebble.error.PebbleException;
+
+import java.util.Map;
+import java.util.Optional;
+
+public class MapResolver implements AttributeResolver {
+
+  @Override
+  public Optional<ResolvedAttribute> resolve(Object instance,
+                                             Object attribute,
+                                             Object[] argumentValues,
+                                             boolean isStrictVariables,
+                                             String filename,
+                                             int lineNumber) {
+    if (argumentValues == null && instance instanceof Map) {
+      return Optional.of(() -> getObjectFromMap((Map<?, ?>) instance, attribute, filename, lineNumber));
+    }
+    return Optional.empty();
+  }
+
+  private static Object getObjectFromMap(Map<?, ?> object,
+                                         Object attributeNameValue,
+                                         String filename,
+                                         int lineNumber) throws PebbleException {
+    if (object.isEmpty()) {
+      return null;
+    }
+    if (attributeNameValue != null && Number.class.isAssignableFrom(attributeNameValue.getClass())) {
+      Number keyAsNumber = (Number) attributeNameValue;
+
+      Class<?> keyClass = object.keySet().iterator().next().getClass();
+      Object key = cast(keyAsNumber, keyClass, filename, lineNumber);
+      return object.get(key);
+    }
+    return object.get(attributeNameValue);
+  }
+
+  private static Object cast(Number number,
+                             Class<?> desiredType,
+                             String filename,
+                             int lineNumber) throws PebbleException {
+    if (desiredType == Long.class) {
+      return number.longValue();
+    } else if (desiredType == Integer.class) {
+      return number.intValue();
+    } else if (desiredType == Double.class) {
+      return number.doubleValue();
+    } else if (desiredType == Float.class) {
+      return number.floatValue();
+    } else if (desiredType == Short.class) {
+      return number.shortValue();
+    } else if (desiredType == Byte.class) {
+      return number.byteValue();
+    }
+    throw new PebbleException(null, String.format("type %s not supported for key %s", desiredType, number), lineNumber, filename);
+  }
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/attributes/MemberResolver.java b/src/main/java/com/mitchellbosecke/pebble/attributes/MemberResolver.java
new file mode 100644
index 00000000..07e180e4
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/attributes/MemberResolver.java
@@ -0,0 +1,260 @@
+package com.mitchellbosecke.pebble.attributes;
+
+import com.mitchellbosecke.pebble.error.ClassAccessException;
+
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class MemberResolver implements AttributeResolver {
+
+  private final ConcurrentHashMap<MemberCacheKey, Member> memberCache = new ConcurrentHashMap<>(100, 0.9f, 1);
+
+  @Override
+  public Optional<ResolvedAttribute> resolve(Object instance,
+                                             Object attribute,
+                                             Object[] argumentValues,
+                                             boolean isStrictVariables,
+                                             String filename,
+                                             int lineNumber) {
+    String attributeName = String.valueOf(attribute);
+    final Member member = this.memberOf(instance, attributeName, argumentValues);
+
+    if (instance != null && member != null) {
+      return Optional.of(() -> this.invokeMember(instance, member, argumentValues));
+    } else if (isStrictVariables) {
+      if (attributeName.equals("class") || attributeName.equals("getClass")) {
+        throw new ClassAccessException(lineNumber, filename);
+      }
+    }
+
+    return Optional.empty();
+  }
+
+  private Member memberOf(Object instance, String attributeName, Object[] nullableArgumentValues) {
+    Object[] argumentValues = nullableArgumentValues;
+    if (argumentValues == null) {
+      argumentValues = new Object[0];
+    }
+
+    MemberCacheKey key = new MemberCacheKey(instance.getClass(), attributeName, argumentValues);
+
+    Member member = this.memberCache.get(key);
+    if (member == null) {
+      /*
+       * turn args into an array of types and an array of values in order
+       * to use them for our reflection calls
+       */
+      Class<?>[] argumentTypes = new Class<?>[argumentValues.length];
+
+      for (int i = 0; i < argumentValues.length; i++) {
+        Object o = argumentValues[i];
+        if (o == null) {
+          argumentTypes[i] = null;
+        } else {
+          argumentTypes[i] = o.getClass();
+        }
+      }
+
+      member = this.reflect(instance, attributeName, argumentTypes);
+      if (member != null) {
+        this.memberCache.put(key, member);
+      }
+
+    }
+
+    return member;
+  }
+
+  /**
+   * Invoke the "Member" that was found via reflection.
+   */
+  private Object invokeMember(Object instance, Member member, Object[] argumentValues) {
+    Object result = null;
+    try {
+      if (member instanceof Method) {
+        result = ((Method) member).invoke(instance, argumentValues);
+      } else if (member instanceof Field) {
+        result = ((Field) member).get(instance);
+      }
+
+    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+      throw new RuntimeException("error invoking " + member + " with " + Arrays.asList(this.classesOf(argumentValues)), e);
+    }
+    return result;
+  }
+
+  /**
+   * Performs the actual reflection to obtain a "Member" from a class.
+   */
+  private Member reflect(Object instance, String attributeName, Class<?>[] parameterTypes) {
+
+    Class<?> clazz = instance.getClass();
+
+    Member result;
+
+    // capitalize first letter of attribute for the following attempts
+    String attributeCapitalized = Character.toUpperCase(attributeName.charAt(0)) + attributeName.substring(1);
+
+    // check get method
+    result = this.findMethod(clazz, "get" + attributeCapitalized, parameterTypes);
+
+    // check is method
+    if (result == null) {
+      result = this.findMethod(clazz, "is" + attributeCapitalized, parameterTypes);
+    }
+
+    // check has method
+    if (result == null) {
+      result = this.findMethod(clazz, "has" + attributeCapitalized, parameterTypes);
+    }
+
+    // check if attribute is a public method
+    if (result == null) {
+      result = this.findMethod(clazz, attributeName, parameterTypes);
+    }
+
+    // public field
+    if (result == null) {
+      try {
+        result = clazz.getField(attributeName);
+      } catch (NoSuchFieldException | SecurityException e) {
+      }
+    }
+
+    if (result != null) {
+      ((AccessibleObject) result).setAccessible(true);
+    }
+
+    return result;
+  }
+
+  /**
+   * Finds an appropriate method by comparing if parameter types are
+   * compatible. This is more relaxed than class.getMethod.
+   */
+  private Method findMethod(Class<?> clazz, String name, Class<?>[] requiredTypes) {
+    if (name.equals("getClass")) {
+      return null;
+    }
+
+    Method result = null;
+
+    Method[] candidates = clazz.getMethods();
+
+    for (Method candidate : candidates) {
+      if (!candidate.getName().equalsIgnoreCase(name)) {
+        continue;
+      }
+
+      Class<?>[] types = candidate.getParameterTypes();
+
+      if (types.length != requiredTypes.length) {
+        continue;
+      }
+
+      boolean compatibleTypes = true;
+      for (int i = 0; i < types.length; i++) {
+        if (requiredTypes[i] != null && !this.widen(types[i]).isAssignableFrom(requiredTypes[i])) {
+          compatibleTypes = false;
+          break;
+        }
+      }
+
+      if (compatibleTypes) {
+        result = candidate;
+        break;
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Performs a widening conversion (primitive to boxed type)
+   */
+  private Class<?> widen(Class<?> clazz) {
+    Class<?> result = clazz;
+    if (clazz == int.class) {
+      result = Integer.class;
+    } else if (clazz == long.class) {
+      result = Long.class;
+    } else if (clazz == double.class) {
+      result = Double.class;
+    } else if (clazz == float.class) {
+      result = Float.class;
+    } else if (clazz == short.class) {
+      result = Short.class;
+    } else if (clazz == byte.class) {
+      result = Byte.class;
+    } else if (clazz == boolean.class) {
+      result = Boolean.class;
+    }
+    return result;
+  }
+
+  private Class<?>[] classesOf(Object[] arguments) {
+    if (arguments != null) {
+      Class<?>[] classes = new Class[arguments.length];
+      for (int i = 0; i < classes.length; i++) {
+        classes[i] = arguments[i] != null ? arguments[i].getClass() : Void.class;
+      }
+      return classes;
+    }
+    return new Class[0];
+  }
+
+  private class MemberCacheKey {
+    private final Class<?> clazz;
+    private final String attributeName;
+    private final List<Class<?>> argumentClasses;
+
+    private MemberCacheKey(Class<?> clazz, String attributeName, Object[] arguments) {
+      this.clazz = clazz;
+      this.attributeName = attributeName;
+      this.argumentClasses = Arrays.asList(MemberResolver.this.classesOf(arguments));
+    }
+
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + ((this.argumentClasses == null) ? 0 : this.argumentClasses.hashCode());
+      result = prime * result + ((this.attributeName == null) ? 0 : this.attributeName.hashCode());
+      result = prime * result + ((this.clazz == null) ? 0 : this.clazz.hashCode());
+      return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj)
+        return true;
+      if (obj == null)
+        return false;
+      if (this.getClass() != obj.getClass())
+        return false;
+      MemberCacheKey other = (MemberCacheKey) obj;
+      if (this.argumentClasses == null) {
+        if (other.argumentClasses != null)
+          return false;
+      } else if (!this.argumentClasses.equals(other.argumentClasses))
+        return false;
+      if (this.attributeName == null) {
+        if (other.attributeName != null)
+          return false;
+      } else if (!this.attributeName.equals(other.attributeName))
+        return false;
+      if (this.clazz == null) {
+        return other.clazz == null;
+      } else return this.clazz.equals(other.clazz);
+    }
+
+
+  }
+
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/attributes/ResolvedAttribute.java b/src/main/java/com/mitchellbosecke/pebble/attributes/ResolvedAttribute.java
new file mode 100644
index 00000000..fdc63ec2
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/attributes/ResolvedAttribute.java
@@ -0,0 +1,5 @@
+package com.mitchellbosecke.pebble.attributes;
+
+public interface ResolvedAttribute {
+  Object evaluate();
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/extension/AbstractExtension.java b/src/main/java/com/mitchellbosecke/pebble/extension/AbstractExtension.java
index 90c01bd5..c464b37e 100644
--- a/src/main/java/com/mitchellbosecke/pebble/extension/AbstractExtension.java
+++ b/src/main/java/com/mitchellbosecke/pebble/extension/AbstractExtension.java
@@ -11,6 +11,7 @@ package com.mitchellbosecke.pebble.extension;
 import java.util.List;
 import java.util.Map;
 
+import com.mitchellbosecke.pebble.attributes.AttributeResolver;
 import com.mitchellbosecke.pebble.operator.BinaryOperator;
 import com.mitchellbosecke.pebble.operator.UnaryOperator;
 import com.mitchellbosecke.pebble.tokenParser.TokenParser;
@@ -56,4 +57,9 @@ public abstract class AbstractExtension implements Extension {
     public List<NodeVisitorFactory> getNodeVisitors() {
         return null;
     }
+    
+    @Override
+    public List<AttributeResolver> getAttributeResolver() {
+        return null;
+    }
 }
diff --git a/src/main/java/com/mitchellbosecke/pebble/extension/Extension.java b/src/main/java/com/mitchellbosecke/pebble/extension/Extension.java
index e8f4b888..fa086eb3 100644
--- a/src/main/java/com/mitchellbosecke/pebble/extension/Extension.java
+++ b/src/main/java/com/mitchellbosecke/pebble/extension/Extension.java
@@ -11,6 +11,7 @@ package com.mitchellbosecke.pebble.extension;
 import java.util.List;
 import java.util.Map;
 
+import com.mitchellbosecke.pebble.attributes.AttributeResolver;
 import com.mitchellbosecke.pebble.operator.BinaryOperator;
 import com.mitchellbosecke.pebble.operator.UnaryOperator;
 import com.mitchellbosecke.pebble.tokenParser.TokenParser;
@@ -75,4 +76,11 @@ public interface Extension {
      * @return a list of node visitors
      */
     List<NodeVisitorFactory> getNodeVisitors();
+    
+    /**
+     * AttributeResolver will resolve instance attributes 
+     * 
+     * @return a list of attribute resolver
+     */
+    List<AttributeResolver> getAttributeResolver();
 }
diff --git a/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionRegistry.java b/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionRegistry.java
index 9c28a5c8..93631ec7 100644
--- a/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionRegistry.java
+++ b/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionRegistry.java
@@ -1,5 +1,6 @@
 package com.mitchellbosecke.pebble.extension;
 
+import com.mitchellbosecke.pebble.attributes.AttributeResolver;
 import com.mitchellbosecke.pebble.operator.BinaryOperator;
 import com.mitchellbosecke.pebble.operator.UnaryOperator;
 import com.mitchellbosecke.pebble.tokenParser.TokenParser;
@@ -21,42 +22,44 @@ public class ExtensionRegistry {
     /**
      * Unary operators used during the lexing phase.
      */
-    private Map<String, UnaryOperator> unaryOperators = new HashMap<>();
+    private final Map<String, UnaryOperator> unaryOperators = new HashMap<>();
 
     /**
      * Binary operators used during the lexing phase.
      */
-    private Map<String, BinaryOperator> binaryOperators = new HashMap<>();
+    private final Map<String, BinaryOperator> binaryOperators = new HashMap<>();
 
     /**
      * Token parsers used during the parsing phase.
      */
-    private Map<String, TokenParser> tokenParsers = new HashMap<>();
+    private final Map<String, TokenParser> tokenParsers = new HashMap<>();
 
     /**
      * Node visitors available during the parsing phase.
      */
-    private List<NodeVisitorFactory> nodeVisitors = new ArrayList<>();
+    private final List<NodeVisitorFactory> nodeVisitors = new ArrayList<>();
 
     /**
      * Filters used during the evaluation phase.
      */
-    private Map<String, Filter> filters = new HashMap<>();
+    private final Map<String, Filter> filters = new HashMap<>();
 
     /**
      * Tests used during the evaluation phase.
      */
-    private Map<String, Test> tests = new HashMap<>();
+    private final Map<String, Test> tests = new HashMap<>();
 
     /**
      * Functions used during the evaluation phase.
      */
-    private Map<String, Function> functions = new HashMap<>();
+    private final Map<String, Function> functions = new HashMap<>();
 
     /**
      * Global variables available during the evaluation phase.
      */
-    private Map<String, Object> globalVariables = new HashMap<>();
+    private final Map<String, Object> globalVariables = new HashMap<>();
+
+    private final List<AttributeResolver> attributeResolver = new ArrayList<>();
 
     public ExtensionRegistry(Collection<? extends Extension> extensions) {
 
@@ -118,6 +121,12 @@ public class ExtensionRegistry {
             if (nodeVisitors != null) {
                 this.nodeVisitors.addAll(nodeVisitors);
             }
+
+            // attribute resolver
+            List<AttributeResolver> attributeResolvers = extension.getAttributeResolver();
+            if (attributeResolvers != null) {
+                this.attributeResolver.addAll(attributeResolvers);
+            }
         }
     }
 
@@ -152,4 +161,8 @@ public class ExtensionRegistry {
     public Map<String, TokenParser> getTokenParsers() {
         return this.tokenParsers;
     }
+
+    public List<AttributeResolver> getAttributeResolver() {
+        return this.attributeResolver;
+    }
 }
diff --git a/src/main/java/com/mitchellbosecke/pebble/extension/core/AttributeResolverExtension.java b/src/main/java/com/mitchellbosecke/pebble/extension/core/AttributeResolverExtension.java
new file mode 100644
index 00000000..35238cde
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/extension/core/AttributeResolverExtension.java
@@ -0,0 +1,17 @@
+package com.mitchellbosecke.pebble.extension.core;
+
+import com.mitchellbosecke.pebble.attributes.AttributeResolver;
+import com.mitchellbosecke.pebble.attributes.DefaultAttributeResolver;
+import com.mitchellbosecke.pebble.extension.AbstractExtension;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AttributeResolverExtension extends AbstractExtension {
+  @Override
+  public List<AttributeResolver> getAttributeResolver() {
+    List<AttributeResolver> attributeResolvers = new ArrayList<>();
+    attributeResolvers.add(new DefaultAttributeResolver());
+    return attributeResolvers;
+  }
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/extension/core/CoreExtension.java b/src/main/java/com/mitchellbosecke/pebble/extension/core/CoreExtension.java
index 89045768..8b9081b2 100644
--- a/src/main/java/com/mitchellbosecke/pebble/extension/core/CoreExtension.java
+++ b/src/main/java/com/mitchellbosecke/pebble/extension/core/CoreExtension.java
@@ -8,10 +8,51 @@
  ******************************************************************************/
 package com.mitchellbosecke.pebble.extension.core;
 
-import com.mitchellbosecke.pebble.extension.*;
-import com.mitchellbosecke.pebble.node.expression.*;
-import com.mitchellbosecke.pebble.operator.*;
-import com.mitchellbosecke.pebble.tokenParser.*;
+import com.mitchellbosecke.pebble.extension.AbstractExtension;
+import com.mitchellbosecke.pebble.extension.Filter;
+import com.mitchellbosecke.pebble.extension.Function;
+import com.mitchellbosecke.pebble.extension.NodeVisitorFactory;
+import com.mitchellbosecke.pebble.extension.Test;
+import com.mitchellbosecke.pebble.node.expression.AddExpression;
+import com.mitchellbosecke.pebble.node.expression.AndExpression;
+import com.mitchellbosecke.pebble.node.expression.ConcatenateExpression;
+import com.mitchellbosecke.pebble.node.expression.ContainsExpression;
+import com.mitchellbosecke.pebble.node.expression.DivideExpression;
+import com.mitchellbosecke.pebble.node.expression.EqualsExpression;
+import com.mitchellbosecke.pebble.node.expression.FilterExpression;
+import com.mitchellbosecke.pebble.node.expression.GreaterThanEqualsExpression;
+import com.mitchellbosecke.pebble.node.expression.GreaterThanExpression;
+import com.mitchellbosecke.pebble.node.expression.LessThanEqualsExpression;
+import com.mitchellbosecke.pebble.node.expression.LessThanExpression;
+import com.mitchellbosecke.pebble.node.expression.ModulusExpression;
+import com.mitchellbosecke.pebble.node.expression.MultiplyExpression;
+import com.mitchellbosecke.pebble.node.expression.NegativeTestExpression;
+import com.mitchellbosecke.pebble.node.expression.NotEqualsExpression;
+import com.mitchellbosecke.pebble.node.expression.OrExpression;
+import com.mitchellbosecke.pebble.node.expression.PositiveTestExpression;
+import com.mitchellbosecke.pebble.node.expression.RangeExpression;
+import com.mitchellbosecke.pebble.node.expression.SubtractExpression;
+import com.mitchellbosecke.pebble.node.expression.UnaryMinusExpression;
+import com.mitchellbosecke.pebble.node.expression.UnaryNotExpression;
+import com.mitchellbosecke.pebble.node.expression.UnaryPlusExpression;
+import com.mitchellbosecke.pebble.operator.Associativity;
+import com.mitchellbosecke.pebble.operator.BinaryOperator;
+import com.mitchellbosecke.pebble.operator.BinaryOperatorImpl;
+import com.mitchellbosecke.pebble.operator.UnaryOperator;
+import com.mitchellbosecke.pebble.operator.UnaryOperatorImpl;
+import com.mitchellbosecke.pebble.tokenParser.BlockTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.CacheTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.ExtendsTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.FilterTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.FlushTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.ForTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.IfTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.ImportTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.IncludeTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.MacroTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.ParallelTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.SetTokenParser;
+import com.mitchellbosecke.pebble.tokenParser.TokenParser;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -143,5 +184,4 @@ public class CoreExtension extends AbstractExtension {
         visitors.add(new MacroAndBlockRegistrantNodeVisitorFactory());
         return visitors;
     }
-
 }
diff --git a/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java b/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java
index 0e53f80f..d9b5cf5a 100644
--- a/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java
+++ b/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java
@@ -1,21 +1,15 @@
 /*******************************************************************************
  * This file is part of Pebble.
- * <<<<<<< HEAD
  * <p>
  * Copyright (c) 2014 by Mitchell Bösecke
  * <p>
- * =======
- * <p>
- * Copyright (c) 2014 by Mitchell Bösecke
- * <p>
- * >>>>>>> d6a41085fe86ce30f23d3b7929ad492343ff01b7
  * For the full copyright and license information, please view the LICENSE
  * file that was distributed with this source code.
  ******************************************************************************/
 package com.mitchellbosecke.pebble.node.expression;
 
+import com.mitchellbosecke.pebble.attributes.ResolvedAttribute;
 import com.mitchellbosecke.pebble.error.AttributeNotFoundException;
-import com.mitchellbosecke.pebble.error.ClassAccessException;
 import com.mitchellbosecke.pebble.error.PebbleException;
 import com.mitchellbosecke.pebble.error.RootAttributeNotFoundException;
 import com.mitchellbosecke.pebble.extension.DynamicAttributeProvider;
@@ -25,15 +19,8 @@ import com.mitchellbosecke.pebble.node.PositionalArgumentNode;
 import com.mitchellbosecke.pebble.template.EvaluationContextImpl;
 import com.mitchellbosecke.pebble.template.PebbleTemplateImpl;
 
-import java.lang.reflect.AccessibleObject;
-import java.lang.reflect.Array;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Member;
-import java.lang.reflect.Method;
 import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.Optional;
 
 /**
  * Used to get an attribute from an object. It will look up attributes in the
@@ -55,11 +42,6 @@ public class GetAttributeExpression implements Expression<Object> {
 
     private final int lineNumber;
 
-    /**
-     * Potentially cached on first evaluation.
-     */
-    private final ConcurrentHashMap<MemberCacheKey, Member> memberCache;
-
     public GetAttributeExpression(Expression<?> node, Expression<?> attributeNameExpression, String filename,
                                   int lineNumber) {
         this(node, attributeNameExpression, null, filename, lineNumber);
@@ -73,194 +55,57 @@ public class GetAttributeExpression implements Expression<Object> {
         this.args = args;
         this.filename = filename;
         this.lineNumber = lineNumber;
-
-        /*
-         * I dont imagine that users will often give different types to the same
-         * template so we will give this cache a pretty small initial capacity.
-         */
-        this.memberCache = new ConcurrentHashMap<>(2, 0.9f, 1);
     }
 
     @Override
-    public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) {
-        Object object = this.node.evaluate(self, context);
-        Object attributeNameValue = this.attributeNameExpression.evaluate(self, context);
-        String attributeName = String.valueOf(attributeNameValue);
-
-        Object result = null;
-
-        Object[] argumentValues = this.getArgumentValues(self, context);
-
-        Member member = object == null ? null : this.memberCache.get(new MemberCacheKey(object.getClass(), attributeName));
-        if (object != null && member == null) {
-
-            /*
-             * If, and only if, no arguments were provided does it make sense to
-             * check maps/arrays/lists
-             */
-            if (this.args == null) {
-
-                // first we check maps
-                if (object instanceof Map) {
-                    return this.getObjectFromMap((Map<?, ?>) object, attributeNameValue);
-                }
-
-                try {
-
-                    // then we check arrays
-                    if (object.getClass().isArray()) {
-                        int index = Integer.parseInt(attributeName);
-                        int length = Array.getLength(object);
-                        if (index < 0 || index >= length) {
-                            if (context.isStrictVariables()) {
-                                throw new AttributeNotFoundException(null,
-                                        "Index out of bounds while accessing array with strict variables on.",
-                                        attributeName, this.lineNumber, this.filename);
-                            } else {
-                                return null;
-                            }
-                        }
-                        return Array.get(object, index);
-                    }
-
-                    // then lists
-                    if (object instanceof List) {
-
-                        @SuppressWarnings("unchecked")
-                        List<Object> list = (List<Object>) object;
-
-                        int index = Integer.parseInt(attributeName);
-                        int length = list.size();
-
-                        if (index < 0 || index >= length) {
-                            if (context.isStrictVariables()) {
-                                throw new AttributeNotFoundException(null,
-                                        "Index out of bounds while accessing array with strict variables on.",
-                                        attributeName, this.lineNumber, this.filename);
-                            } else {
-                                return null;
-                            }
-                        }
-
-                        return list.get(index);
-                    }
-                } catch (NumberFormatException ex) {
-                    // do nothing
-                }
-
-            }
-
-            // check if the object is able to provide the attribute dynamically
-            if (object instanceof DynamicAttributeProvider) {
-                DynamicAttributeProvider dynamicAttributeProvider = (DynamicAttributeProvider) object;
-                if (dynamicAttributeProvider.canProvideDynamicAttribute(attributeName)) {
-                    return dynamicAttributeProvider.getDynamicAttribute(attributeNameValue, argumentValues);
-                }
-            }
-
-            /*
-             * turn args into an array of types and an array of values in order
-             * to use them for our reflection calls
-             */
-            Class<?>[] argumentTypes = new Class<?>[argumentValues.length];
-
-            for (int i = 0; i < argumentValues.length; i++) {
-                Object o = argumentValues[i];
-                if (o == null) {
-                    argumentTypes[i] = null;
-                } else {
-                    argumentTypes[i] = o.getClass();
-                }
-            }
-
-            member = this.reflect(object, attributeName, argumentTypes);
-            if (member != null) {
-                this.memberCache.put(new MemberCacheKey(object.getClass(), attributeName), member);
-            }
-
-        }
-
-        if (object != null && member != null) {
-            result = this.invokeMember(object, member, argumentValues);
-        } else if (context.isStrictVariables()) {
-            if (object == null) {
-
-                if (this.node instanceof ContextVariableExpression) {
-                    final String rootPropertyName = ((ContextVariableExpression) this.node).getName();
-                    throw new RootAttributeNotFoundException(null, String.format(
-                            "Root attribute [%s] does not exist or can not be accessed and strict variables is set to true.",
-                            rootPropertyName), rootPropertyName, this.lineNumber, this.filename);
-                } else {
-                    throw new RootAttributeNotFoundException(null,
-                            "Attempt to get attribute of null object and strict variables is set to true.", attributeName, this.lineNumber, this.filename);
-                }
-
+    public Object evaluate(PebbleTemplateImpl self, EvaluationContextImpl context) throws PebbleException {
+        final Object object = this.node.evaluate(self, context);
+        final Object attributeNameValue = this.attributeNameExpression.evaluate(self, context);
+        final String attributeName = String.valueOf(attributeNameValue);
+        final Object[] argumentValues = this.getArgumentValues(self, context);
+
+        if (object == null && context.isStrictVariables()) {
+            if (this.node instanceof ContextVariableExpression) {
+                final String rootPropertyName = ((ContextVariableExpression) this.node).getName();
+                throw new RootAttributeNotFoundException(null, String.format(
+                        "Root attribute [%s] does not exist or can not be accessed and strict variables is set to true.",
+                        rootPropertyName), rootPropertyName, this.lineNumber, this.filename);
             } else {
-                if (attributeName.equals("class") || attributeName.equals("getClass")) {
-                    throw new ClassAccessException(this.lineNumber, this.filename);
-                } else {
-                    throw new AttributeNotFoundException(null, String.format(
-                            "Attribute [%s] of [%s] does not exist or can not be accessed and strict variables is set to true.",
-                            attributeName, object.getClass().getName()), attributeName, this.lineNumber, this.filename);
-                }
+                throw new RootAttributeNotFoundException(null,
+                        "Attempt to get attribute of null object and strict variables is set to true.", attributeName, this.lineNumber, this.filename);
             }
         }
-        return result;
 
-    }
+        Optional<ResolvedAttribute> resolvedAttribute = context.getExtensionRegistry().getAttributeResolver().stream()
+                .map(attributeResolver -> attributeResolver.resolve(object, attributeNameValue, argumentValues, context.isStrictVariables(), this.filename, this.lineNumber))
+                .filter(Optional::isPresent)
+                .findFirst()
+                .map(Optional::get);
 
-    private Object getObjectFromMap(Map<?, ?> object, Object attributeNameValue) {
-        if (object.isEmpty()) {
-            return null;
+        if (resolvedAttribute.isPresent()) {
+            return resolvedAttribute.get().evaluate();
         }
-        if (attributeNameValue != null && Number.class.isAssignableFrom(attributeNameValue.getClass())) {
-            Number keyAsNumber = (Number) attributeNameValue;
 
-            Class<?> keyClass = object.keySet().iterator().next().getClass();
-            Object key = this.cast(keyAsNumber, keyClass);
-            return object.get(key);
+        if (context.isStrictVariables()) {
+            throw new AttributeNotFoundException(null, String.format(
+                    "Attribute [%s] of [%s] does not exist or can not be accessed and strict variables is set to true.",
+                    attributeName,
+                    object != null ? object.getClass().getName(): null),
+                    attributeName,
+                    this.lineNumber,
+                    this.filename);
         }
-        return object.get(attributeNameValue);
+        return null;
     }
 
-    private Object cast(Number number, Class<?> desiredType) {
-        if (desiredType == Long.class) {
-            return number.longValue();
-        } else if (desiredType == Integer.class) {
-            return number.intValue();
-        } else if (desiredType == Double.class) {
-            return number.doubleValue();
-        } else if (desiredType == Float.class) {
-            return number.floatValue();
-        } else if (desiredType == Short.class) {
-            return number.shortValue();
-        } else if (desiredType == Byte.class) {
-            return number.byteValue();
+    private Object checkStrictVariableOrElseReturnNull(EvaluationContextImpl context, Object object, String attributeName) {
+        if (context.isStrictVariables()) {
+            throw new AttributeNotFoundException(null, String.format(
+                    "Attribute [%s] of [%s] does not exist or can not be accessed and strict variables is set to true.",
+                    attributeName, object.getClass().getName()), attributeName, this.lineNumber, this.filename);
         }
-        throw new PebbleException(null, String.format("type %s not supported for key %s", desiredType, number), this.getLineNumber(), this.filename);
-    }
 
-    /**
-     * Invoke the "Member" that was found via reflection.
-     *
-     * @param object
-     * @param member
-     * @param argumentValues
-     * @return
-     */
-    private Object invokeMember(Object object, Member member, Object[] argumentValues) {
-        Object result = null;
-        try {
-            if (member instanceof Method) {
-                result = ((Method) member).invoke(object, argumentValues);
-            } else if (member instanceof Field) {
-                result = ((Field) member).get(object);
-            }
-
-        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
-            throw new RuntimeException(e);
-        }
-        return result;
+        return null;
     }
 
     /**
@@ -275,7 +120,7 @@ public class GetAttributeExpression implements Expression<Object> {
         Object[] argumentValues;
 
         if (this.args == null) {
-            argumentValues = new Object[0];
+            argumentValues = null; //new Object[0];
         } else {
             List<PositionalArgumentNode> args = this.args.getPositionalArgs();
 
@@ -291,156 +136,6 @@ public class GetAttributeExpression implements Expression<Object> {
         return argumentValues;
     }
 
-    /**
-     * Performs the actual reflection to obtain a "Member" from a class.
-     *
-     * @param object
-     * @param attributeName
-     * @param parameterTypes
-     * @return
-     */
-    private Member reflect(Object object, String attributeName, Class<?>[] parameterTypes) {
-
-        Class<?> clazz = object.getClass();
-
-        Member result = null;
-
-        // capitalize first letter of attribute for the following attempts
-        String attributeCapitalized = Character.toUpperCase(attributeName.charAt(0)) + attributeName.substring(1);
-
-        // check get method
-        result = this.findMethod(clazz, "get" + attributeCapitalized, parameterTypes);
-
-        // check is method
-        if (result == null) {
-            result = this.findMethod(clazz, "is" + attributeCapitalized, parameterTypes);
-        }
-
-        // check has method
-        if (result == null) {
-            result = this.findMethod(clazz, "has" + attributeCapitalized, parameterTypes);
-        }
-
-        // check if attribute is a public method
-        if (result == null) {
-            result = this.findMethod(clazz, attributeName, parameterTypes);
-        }
-
-        // public field
-        if (result == null) {
-            try {
-                result = clazz.getField(attributeName);
-            } catch (NoSuchFieldException | SecurityException e) {
-            }
-        }
-
-        if (result != null) {
-            ((AccessibleObject) result).setAccessible(true);
-        }
-
-        return result;
-    }
-
-    /**
-     * Finds an appropriate method by comparing if parameter types are
-     * compatible. This is more relaxed than class.getMethod.
-     *
-     * @param clazz
-     * @param name
-     * @param requiredTypes
-     * @return
-     */
-    private Method findMethod(Class<?> clazz, String name, Class<?>[] requiredTypes) {
-        if (name.equals("getClass")) {
-            return null;
-        }
-
-        Method result = null;
-
-        Method[] candidates = clazz.getMethods();
-
-        for (Method candidate : candidates) {
-            if (!candidate.getName().equalsIgnoreCase(name)) {
-                continue;
-            }
-
-            Class<?>[] types = candidate.getParameterTypes();
-
-            if (types.length != requiredTypes.length) {
-                continue;
-            }
-
-            boolean compatibleTypes = true;
-            for (int i = 0; i < types.length; i++) {
-                if (requiredTypes[i] != null && !this.widen(types[i]).isAssignableFrom(requiredTypes[i])) {
-                    compatibleTypes = false;
-                    break;
-                }
-            }
-
-            if (compatibleTypes) {
-                result = candidate;
-                break;
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Performs a widening conversion (primitive to boxed type)
-     *
-     * @param clazz
-     * @return
-     */
-    private Class<?> widen(Class<?> clazz) {
-        Class<?> result = clazz;
-        if (clazz == int.class) {
-            result = Integer.class;
-        } else if (clazz == long.class) {
-            result = Long.class;
-        } else if (clazz == double.class) {
-            result = Double.class;
-        } else if (clazz == float.class) {
-            result = Float.class;
-        } else if (clazz == short.class) {
-            result = Short.class;
-        } else if (clazz == byte.class) {
-            result = Byte.class;
-        } else if (clazz == boolean.class) {
-            result = Boolean.class;
-        }
-        return result;
-    }
-
-    private class MemberCacheKey {
-        private final Class<?> clazz;
-        private final String attributeName;
-
-        private MemberCacheKey(Class<?> clazz, String attributeName) {
-            this.clazz = clazz;
-            this.attributeName = attributeName;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || this.getClass() != o.getClass()) return false;
-
-            MemberCacheKey that = (MemberCacheKey) o;
-
-            if (!this.clazz.equals(that.clazz)) return false;
-            return this.attributeName.equals(that.attributeName);
-
-        }
-
-        @Override
-        public int hashCode() {
-            int result = this.clazz.hashCode();
-            result = 31 * result + this.attributeName.hashCode();
-            return result;
-        }
-    }
-
     @Override
     public void accept(NodeVisitor visitor) {
         visitor.visit(this);


Test patch:
diff --git a/src/test/java/com/mitchellbosecke/pebble/ExtendingPebbleTest.java b/src/test/java/com/mitchellbosecke/pebble/ExtendingPebbleTest.java
index 7b434d22..f7704f0e 100644
--- a/src/test/java/com/mitchellbosecke/pebble/ExtendingPebbleTest.java
+++ b/src/test/java/com/mitchellbosecke/pebble/ExtendingPebbleTest.java
@@ -8,27 +8,65 @@
  ******************************************************************************/
 package com.mitchellbosecke.pebble;
 
+import com.mitchellbosecke.pebble.attributes.AttributeResolver;
 import com.mitchellbosecke.pebble.error.PebbleException;
 import com.mitchellbosecke.pebble.extension.AbstractExtension;
 import com.mitchellbosecke.pebble.extension.Filter;
 import com.mitchellbosecke.pebble.loader.StringLoader;
 import com.mitchellbosecke.pebble.template.EvaluationContext;
 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.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 import static org.junit.Assert.assertEquals;
 
 public class ExtendingPebbleTest extends AbstractTest {
 
-    private final class CustomExtension extends AbstractExtension {
+    /**
+     * Issue #51
+     */
+    @Test
+    public void testFilterWithoutArgumentsCanAccessEvaluationContext() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder()
+                .loader(new StringLoader())
+                .strictVariables(false)
+                .extension(new CustomExtensionWithFilter())
+                .build();
+
+        PebbleTemplate template = pebble.getTemplate("{{ 'test' | noArgumentsButCanAccessContext }}");
+
+        Writer writer = new StringWriter();
+        template.evaluate(writer);
+        assertEquals("success", writer.toString());
+    }
+
+    @Test
+    public void testCustomAttributeResolverEvaluateFirst() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder()
+                .loader(new StringLoader())
+                .strictVariables(false)
+                .extension(new CustomExtensionWithAttributeResolver())
+                .build();
+
+        PebbleTemplate template = pebble.getTemplate("hello {{ person.name }}");
+        Map<String, Object> context = new HashMap<>();
+        context.put("person", new SimplePerson());
 
+        Writer writer = new StringWriter();
+        template.evaluate(writer, context);
+        assertEquals("hello customAttributeResolver", writer.toString());
+    }
+
+    private static final class CustomExtensionWithFilter extends AbstractExtension {
         @Override
         public Map<String, Filter> getFilters() {
 
@@ -55,18 +93,18 @@ public class ExtendingPebbleTest extends AbstractTest {
         }
     }
 
-    /**
-     * Issue #51
-     */
-    @Test
-    public void testFilterWithoutArgumentsCanAccessEvaluationContext() throws PebbleException, IOException {
-        PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false)
-                .extension(new CustomExtension()).build();
+    private static final class CustomExtensionWithAttributeResolver extends AbstractExtension {
+        @Override
+        public List<AttributeResolver> getAttributeResolver() {
 
-        PebbleTemplate template = pebble.getTemplate("{{ 'test' | noArgumentsButCanAccessContext }}");
+            List<AttributeResolver> attributeResolvers = new ArrayList<>();
+            attributeResolvers.add((instance, attribute, argumentValues, isStrictVariables, filename, lineNumber) ->
+                    Optional.of(() -> "customAttributeResolver"));
+            return attributeResolvers;
+        }
+    }
 
-        Writer writer = new StringWriter();
-        template.evaluate(writer);
-        assertEquals("success", writer.toString());
+    private static class SimplePerson {
+        public final String name = "Bob";
     }
 }
diff --git a/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java b/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java
index 54c3a3a4..779d8992 100644
--- a/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java
+++ b/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java
@@ -17,6 +17,7 @@ import com.mitchellbosecke.pebble.loader.StringLoader;
 import com.mitchellbosecke.pebble.template.PebbleTemplate;
 
 import org.junit.Test;
+import static org.junit.Assert.assertEquals;
 
 import java.io.IOException;
 import java.io.StringWriter;
@@ -28,7 +29,14 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+import com.mitchellbosecke.pebble.error.AttributeNotFoundException;
+import com.mitchellbosecke.pebble.error.PebbleException;
+import com.mitchellbosecke.pebble.error.RootAttributeNotFoundException;
+import com.mitchellbosecke.pebble.extension.DynamicAttributeProvider;
+import com.mitchellbosecke.pebble.loader.StringLoader;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
 
 public class GetAttributeTest extends AbstractTest {