From fe4d8fed2f049e230efaacac0fe774df58a65c5d Mon Sep 17 00:00:00 2001 From: Eric Bussieres Date: Thu, 27 Apr 2017 06:11:05 -0400 Subject: [PATCH] Accessing map via primitive (#196) --- .../expression/GetAttributeExpression.java | 94 +++++++++++++------ .../pebble/GetAttributeTest.java | 75 ++++++++++++++- 2 files changed, 138 insertions(+), 31 deletions(-) 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 cb8c9425e..9717d1496 100644 --- a/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java +++ b/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java @@ -23,7 +23,12 @@ import com.mitchellbosecke.pebble.template.EvaluationContext; import com.mitchellbosecke.pebble.template.PebbleTemplateImpl; -import java.lang.reflect.*; +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; @@ -75,15 +80,15 @@ public GetAttributeExpression(Expression node, Expression attributeNameExp @Override public Object evaluate(PebbleTemplateImpl self, EvaluationContext context) throws PebbleException { - Object object = node.evaluate(self, context); - Object attributeNameValue = attributeNameExpression.evaluate(self, 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 = null; - Member member = object == null ? null : memberCache.get(new MemberCacheKey(object.getClass(), attributeName)); + Member member = object == null ? null : this.memberCache.get(new MemberCacheKey(object.getClass(), attributeName)); if (object != null && member == null) { @@ -91,11 +96,11 @@ public Object evaluate(PebbleTemplateImpl self, EvaluationContext context) throw * If, and only if, no arguments were provided does it make sense to * check maps/arrays/lists */ - if (args == null) { + if (this.args == null) { // first we check maps - if (object instanceof Map && ((Map) object).containsKey(attributeNameValue)) { - return ((Map) object).get(attributeNameValue); + if (object instanceof Map) { + return this.getObjectFromMap((Map) object, attributeNameValue); } try { @@ -108,7 +113,7 @@ public Object evaluate(PebbleTemplateImpl self, EvaluationContext context) throw if (context.isStrictVariables()) { throw new AttributeNotFoundException(null, "Index out of bounds while accessing array with strict variables on.", - attributeName, lineNumber, filename); + attributeName, this.lineNumber, this.filename); } else { return null; } @@ -129,7 +134,7 @@ public Object evaluate(PebbleTemplateImpl self, EvaluationContext context) throw if (context.isStrictVariables()) { throw new AttributeNotFoundException(null, "Index out of bounds while accessing array with strict variables on.", - attributeName, lineNumber, filename); + attributeName, this.lineNumber, this.filename); } else { return null; } @@ -147,7 +152,7 @@ public Object evaluate(PebbleTemplateImpl self, EvaluationContext context) throw * turn args into an array of types and an array of values in order * to use them for our reflection calls */ - argumentValues = getArgumentValues(self, context); + argumentValues = this.getArgumentValues(self, context); Class[] argumentTypes = new Class[argumentValues.length]; for (int i = 0; i < argumentValues.length; i++) { @@ -159,23 +164,23 @@ public Object evaluate(PebbleTemplateImpl self, EvaluationContext context) throw } } - member = reflect(object, attributeName, argumentTypes); + member = this.reflect(object, attributeName, argumentTypes); if (member != null) { - memberCache.put(new MemberCacheKey(object.getClass(), attributeName), member); + this.memberCache.put(new MemberCacheKey(object.getClass(), attributeName), member); } } if (object != null && member != null) { if (argumentValues == null) { - argumentValues = getArgumentValues(self, context); + argumentValues = this.getArgumentValues(self, context); } - result = invokeMember(object, member, argumentValues); + result = this.invokeMember(object, member, argumentValues); } else if (context.isStrictVariables()) { if (object == null) { - if (node instanceof ContextVariableExpression) { - final String rootPropertyName = ((ContextVariableExpression) node).getName(); + 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); @@ -194,6 +199,37 @@ public Object evaluate(PebbleTemplateImpl self, EvaluationContext context) throw } + private Object getObjectFromMap(Map object, Object attributeNameValue) throws PebbleException { + if (object.isEmpty()) { + return null; + } + if (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); + } + return object.get(attributeNameValue); + } + + private Object cast(Number number, Class desiredType) 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), this.getLineNumber(), this.filename); + } + /** * Invoke the "Member" that was found via reflection. * @@ -264,21 +300,21 @@ private Member reflect(Object object, String attributeName, Class[] parameter String attributeCapitalized = Character.toUpperCase(attributeName.charAt(0)) + attributeName.substring(1); // check get method - result = findMethod(clazz, "get" + attributeCapitalized, parameterTypes); + result = this.findMethod(clazz, "get" + attributeCapitalized, parameterTypes); // check is method if (result == null) { - result = findMethod(clazz, "is" + attributeCapitalized, parameterTypes); + result = this.findMethod(clazz, "is" + attributeCapitalized, parameterTypes); } // check has method if (result == null) { - result = findMethod(clazz, "has" + attributeCapitalized, parameterTypes); + result = this.findMethod(clazz, "has" + attributeCapitalized, parameterTypes); } // check if attribute is a public method if (result == null) { - result = findMethod(clazz, attributeName, parameterTypes); + result = this.findMethod(clazz, attributeName, parameterTypes); } // public field @@ -323,7 +359,7 @@ private Method findMethod(Class clazz, String name, Class[] requiredTypes) boolean compatibleTypes = true; for (int i = 0; i < types.length; i++) { - if (requiredTypes[i] != null && !widen(types[i]).isAssignableFrom(requiredTypes[i])) { + if (requiredTypes[i] != null && !this.widen(types[i]).isAssignableFrom(requiredTypes[i])) { compatibleTypes = false; break; } @@ -375,19 +411,19 @@ private MemberCacheKey(Class clazz, String attributeName) { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (o == null || this.getClass() != o.getClass()) return false; MemberCacheKey that = (MemberCacheKey) o; - if (!clazz.equals(that.clazz)) return false; - return attributeName.equals(that.attributeName); + if (!this.clazz.equals(that.clazz)) return false; + return this.attributeName.equals(that.attributeName); } @Override public int hashCode() { - int result = clazz.hashCode(); - result = 31 * result + attributeName.hashCode(); + int result = this.clazz.hashCode(); + result = 31 * result + this.attributeName.hashCode(); return result; } } @@ -398,15 +434,15 @@ public void accept(NodeVisitor visitor) { } public Expression getNode() { - return node; + return this.node; } public Expression getAttributeNameExpression() { - return attributeNameExpression; + return this.attributeNameExpression; } public ArgumentsNode getArgumentsNode() { - return args; + return this.args; } @Override diff --git a/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java b/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java index 8471b721a..11ab378a7 100644 --- a/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java +++ b/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java @@ -13,12 +13,14 @@ 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.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -505,8 +507,8 @@ public class ComplexObject { public final Map map = new HashMap<>(); { - map.put("SimpleObject2", new SimpleObject2()); - map.put("SimpleObject6", new SimpleObject6()); + this.map.put("SimpleObject2", new SimpleObject2()); + this.map.put("SimpleObject6", new SimpleObject6()); } } @@ -579,4 +581,73 @@ public String getStringFromBoolean(boolean bool) { } } + @Test + public void testAttributePrimitiveAccessWithEmptyMap() throws Exception { + PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); + + PebbleTemplate template = pebble.getTemplate(String.format("hello {{ object[1].name }}")); + Map context = new HashMap<>(); + context.put("object", new HashMap<>()); + + Writer writer = new StringWriter(); + template.evaluate(writer, context); + + assertEquals("hello ", writer.toString()); + } + + @Test + public void testAttributePrimitiveAccessWithInteger() throws Exception { + String result = this.testAttributePrimitiveAccess(1); + + assertEquals("hello Steve", result); + } + + private String testAttributePrimitiveAccess(Number value) throws PebbleException, IOException { + PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build(); + + PebbleTemplate template = pebble.getTemplate("hello {{ object[key].name }}"); + Map context = new HashMap<>(); + context.put("key", value); + context.put("object", Collections.singletonMap(value, new SimpleObject())); + + Writer writer = new StringWriter(); + template.evaluate(writer, context); + + return writer.toString(); + } + + @Test + public void testAttributePrimitiveAccessWithLong() throws Exception { + String result = this.testAttributePrimitiveAccess(1L); + + assertEquals("hello Steve", result); + } + + @Test + public void testAttributePrimitiveAccessWithDouble() throws Exception { + String result = this.testAttributePrimitiveAccess(1.05D); + + assertEquals("hello Steve", result); + } + + @Test + public void testAttributePrimitiveAccessWithFloat() throws Exception { + String result = this.testAttributePrimitiveAccess(1.05F); + + assertEquals("hello Steve", result); + } + + @Test + public void testAttributePrimitiveAccessWithShort() throws Exception { + String result = this.testAttributePrimitiveAccess((short) 1); + + assertEquals("hello Steve", result); + } + + @Test + public void testAttributePrimitiveAccessWithByte() throws Exception { + String result = this.testAttributePrimitiveAccess((byte) 1); + + assertEquals("hello Steve", result); + } }