Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class JinjavaInterpreterResolver extends SimpleResolver {
add(new JinjavaListELResolver(true));
add(new TypeConvertingMapELResolver(true));
add(new ResourceBundleELResolver());
add(new JinjavaStringELResolver());
add(new JinjavaBeanELResolver(true));
}
};
Expand All @@ -68,6 +69,7 @@ public class JinjavaInterpreterResolver extends SimpleResolver {
add(new JinjavaListELResolver(false));
add(new TypeConvertingMapELResolver(false));
add(new ResourceBundleELResolver());
add(new JinjavaStringELResolver());
add(new JinjavaBeanELResolver(false));
}
};
Expand Down
103 changes: 103 additions & 0 deletions src/main/java/com/hubspot/jinjava/el/JinjavaStringELResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hubspot.jinjava.el;

import java.beans.FeatureDescriptor;
import java.util.Iterator;
import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.PropertyNotWritableException;

class JinjavaStringELResolver extends ELResolver {

@Override
public Object getValue(ELContext context, Object base, Object property) {
if (!isResolvable(base, property)) {
return null;
}
context.setPropertyResolved(true);

int index;
try {
index = Integer.parseInt(property.toString());
} catch (NumberFormatException e) {
return null;
}

String value = (String) base;
if (index < 0) {
index += value.length();
}
// Match the UTF-16 indexing used by string slices and the length filter.
return index >= 0 && index < value.length()
? value.substring(index, index + 1)
: null;
}

@Override
public Class<?> getType(ELContext context, Object base, Object property) {
if (isResolvable(base, property)) {
context.setPropertyResolved(true);
return String.class;
}
return null;
}

@Override
public boolean isReadOnly(ELContext context, Object base, Object property) {
if (isResolvable(base, property)) {
context.setPropertyResolved(true);
return true;
}
return false;
}

@Override
public void setValue(ELContext context, Object base, Object property, Object value) {
if (isResolvable(base, property)) {
context.setPropertyResolved(true);
throw new PropertyNotWritableException("String indices are read-only");
}
}

@Override
public Class<?> getCommonPropertyType(ELContext context, Object base) {
return base instanceof String ? Integer.class : null;
}

@Override
public Iterator<FeatureDescriptor> getFeatureDescriptors(
ELContext context,
Object base
) {
return null;
}

private static boolean isResolvable(Object base, Object property) {
if (!(base instanceof String) || property == null) {
return false;
}
String index = property.toString();
int start = index.startsWith("-") ? 1 : 0;
if (start == index.length()) {
return false;
}
for (int i = start; i < index.length(); i++) {
if (!Character.isDigit(index.charAt(i))) {
return false;
}
}
return true;
}
}
87 changes: 87 additions & 0 deletions src/test/java/com/hubspot/jinjava/el/ExpressionResolverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.hubspot.jinjava.interpret.TemplateError;
import com.hubspot.jinjava.interpret.TemplateError.ErrorItem;
import com.hubspot.jinjava.interpret.TemplateError.ErrorReason;
import com.hubspot.jinjava.objects.SafeString;
import com.hubspot.jinjava.objects.date.PyishDate;
import com.hubspot.jinjava.testobjects.ExpressionResolverTestObjects;
import java.math.BigDecimal;
Expand All @@ -27,6 +28,8 @@
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import javax.el.CompositeELResolver;
import javax.el.ELContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
Expand Down Expand Up @@ -102,6 +105,90 @@ public void itResolvesMathVals() {
assertThat(interpreter.getContext().wasValueResolved("i_am_seven")).isTrue();
}

@Test
public void itResolvesStringIndex() {
RenderResult result = jinjava.renderForResult(
"{{ x[0] }}",
ImmutableMap.of("x", "stuff")
);
assertThat(result.getOutput()).isEqualTo("s");
assertThat(result.getErrors()).isEmpty();
}

@Test
public void itResolvesStringIndicesInExpressions() {
RenderResult result = jinjava.renderForResult(
"{{ 'stuff'[1] }}|{{ x[-1] }}|{{ x['0']|upper }}|{{ x[-5] }}|{{ x[index] }}",
ImmutableMap.of("x", "stuff", "index", 2)
);
assertThat(result.getOutput()).isEqualTo("t|f|S|s|u");
assertThat(result.getErrors()).isEmpty();
}

@Test
public void itResolvesMissingStringIndicesWithoutUnknownPropertyErrors() {
RenderResult result = jinjava.renderForResult(
"{{ x[5] }}{{ x[-6] }}{{ ''[0] }}{{ x['999999999999999999999999'] }}",
ImmutableMap.of("x", "stuff")
);
assertThat(result.getOutput()).isEmpty();
assertThat(result.getErrors()).isEmpty();
}

@Test
public void itPreservesStringMethodsAndSlices() {
RenderResult result = jinjava.renderForResult(
"{{ x.isEmpty() }}|{{ x.length() }}|{{ x[0:1] }}",
ImmutableMap.of("x", "stuff")
);
assertThat(result.getOutput()).isEqualTo("false|5|s");
assertThat(result.getErrors()).isEmpty();
}

@Test
public void itPreservesOtherIndexedValuesAndSafeStringProperties() {
RenderResult result = jinjava.renderForResult(
"{{ list[0] }}|{{ map[0] }}|{{ array[0] }}|{{ safe.value }}",
ImmutableMap.of(
"list",
Lists.newArrayList("list"),
"map",
ImmutableMap.of("0", "map"),
"array",
new String[] { "array" },
"safe",
new SafeString("safe")
)
);
assertThat(result.getOutput()).isEqualTo("list|map|array|safe");
assertThat(result.getErrors()).isEmpty();
}

@Test
public void itUsesCustomResolversForStringIndices() {
Jinjava customJinjava = new Jinjava(
BaseJinjavaTest
.newConfigBuilder()
.withElResolver(
new CompositeELResolver() {
@Override
public Object getValue(ELContext elContext, Object base, Object property) {
if (base instanceof String && "0".equals(property)) {
elContext.setPropertyResolved(true);
return "custom";
}
return super.getValue(elContext, base, property);
}
}
)
.build()
);
JinjavaInterpreter customInterpreter = customJinjava.newInterpreter();
customInterpreter.getContext().put("x", "stuff");
assertThat(customInterpreter.render("{{ x[0] }}")).isEqualTo("custom");
assertThat(customInterpreter.getErrors()).isEmpty();
}

@Test
public void itResolvesListVal() {
context.put("thelist", Lists.newArrayList(1L, 2L, 3L));
Expand Down
143 changes: 143 additions & 0 deletions src/test/java/com/hubspot/jinjava/el/JinjavaStringELResolverTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hubspot.jinjava.el;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.hubspot.jinjava.objects.SafeString;
import de.odysseus.el.util.SimpleContext;
import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.PropertyNotWritableException;
import org.junit.Test;

public class JinjavaStringELResolverTest {

private final JinjavaStringELResolver resolver = new JinjavaStringELResolver();

@Test
public void itResolvesPositiveAndNegativeIndices() {
for (Object index : new Object[] { 0, 0L, "0", "-0", -5, "-5" }) {
assertThat(resolve("stuff", index)).isEqualTo("s");
}
for (Object index : new Object[] { 4, "4", -1L, "-1" }) {
assertThat(resolve("stuff", index)).isEqualTo("f");
}
}

@Test
public void itReturnsNullOutsideTheString() {
for (Object index : new Object[] {
5,
-6,
Integer.MAX_VALUE,
Integer.MIN_VALUE,
Long.MAX_VALUE,
Long.MIN_VALUE,
"999999999999999999999999999999",
"-999999999999999999999999999999",
}) {
assertThat(resolve("stuff", index)).isNull();
}
assertThat(resolve("", 0)).isNull();
assertThat(resolve("", -1)).isNull();
}

@Test
public void itUsesTheSameCharacterBoundariesAsStringSlices() {
String value = "ä\uD83D\uDE00z";
for (int i = 0; i < value.length(); i++) {
assertThat(resolve(value, i)).isEqualTo(value.substring(i, i + 1));
assertThat(resolve(value, i - value.length())).isEqualTo(value.substring(i, i + 1));
}
}

@Test
public void itLeavesNonIndexPropertiesUnresolved() {
for (Object property : new Object[] {
null,
"",
"-",
"--1",
"1-2",
"+1",
"1.5",
1.5,
true,
"length",
"empty",
}) {
assertUnresolved("stuff", property);
}
}

@Test
public void itLeavesOtherTypesUnresolved() {
for (Object base : new Object[] {
null,
123,
ImmutableList.of("s"),
ImmutableMap.of("0", "s"),
new String[] { "s" },
new StringBuilder("stuff"),
new SafeString("stuff"),
}) {
assertUnresolved(base, "0");
}
}

@Test
public void itKeepsStringsReadOnlyInBothDefaultResolvers() {
for (ELResolver defaultResolver : new ELResolver[] {
JinjavaInterpreterResolver.DEFAULT_RESOLVER_READ_ONLY,
JinjavaInterpreterResolver.DEFAULT_RESOLVER_READ_WRITE,
}) {
ELContext context = new SimpleContext();
assertThat(defaultResolver.getValue(context, "stuff", "0")).isEqualTo("s");
assertThat(context.isPropertyResolved()).isTrue();
context.setPropertyResolved(false);
assertThat(defaultResolver.getType(context, "stuff", "0")).isEqualTo(String.class);
assertThat(context.isPropertyResolved()).isTrue();
context.setPropertyResolved(false);
assertThat(defaultResolver.isReadOnly(context, "stuff", "0")).isTrue();
assertThat(context.isPropertyResolved()).isTrue();
context.setPropertyResolved(false);
assertThatThrownBy(() -> defaultResolver.setValue(context, "stuff", "0", "x"))
.isInstanceOf(PropertyNotWritableException.class);
assertThat(context.isPropertyResolved()).isTrue();
}
}

private Object resolve(String base, Object property) {
ELContext context = new SimpleContext();
Object result = resolver.getValue(context, base, property);
assertThat(context.isPropertyResolved()).isTrue();
return result;
}

private void assertUnresolved(Object base, Object property) {
ELContext context = new SimpleContext();
assertThat(resolver.getValue(context, base, property)).isNull();
assertThat(context.isPropertyResolved()).isFalse();
assertThat(resolver.getType(context, base, property)).isNull();
assertThat(context.isPropertyResolved()).isFalse();
assertThat(resolver.isReadOnly(context, base, property)).isFalse();
assertThat(context.isPropertyResolved()).isFalse();
resolver.setValue(context, base, property, "x");
assertThat(context.isPropertyResolved()).isFalse();
}
}
Loading