Skip to content

Commit a3b9053

Browse files
committed
Add configurable random number generator
1 parent b5bee8e commit a3b9053

7 files changed

Lines changed: 193 additions & 20 deletions

File tree

src/main/java/com/hubspot/jinjava/JinjavaConfig.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import com.hubspot.jinjava.interpret.Context;
2828
import com.hubspot.jinjava.interpret.Context.Library;
29+
import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy;
2930

3031
public class JinjavaConfig {
3132

@@ -44,17 +45,18 @@ public class JinjavaConfig {
4445
private Map<Context.Library, Set<String>> disabled;
4546
private final boolean failOnUnknownTokens;
4647
private final boolean nestedInterpretationEnabled;
48+
private final RandomNumberGeneratorStrategy randomNumberGenerator;
4749

4850
public static Builder newBuilder() {
4951
return new Builder();
5052
}
5153

5254
public JinjavaConfig() {
53-
this(StandardCharsets.UTF_8, Locale.ENGLISH, ZoneOffset.UTC, 10, new HashMap<>(), false, false, true, false, false, 0, true);
55+
this(StandardCharsets.UTF_8, Locale.ENGLISH, ZoneOffset.UTC, 10, new HashMap<>(), false, false, true, false, false, 0, true, RandomNumberGeneratorStrategy.THREAD_LOCAL);
5456
}
5557

5658
public JinjavaConfig(Charset charset, Locale locale, ZoneId timeZone, int maxRenderDepth) {
57-
this(charset, locale, timeZone, maxRenderDepth, new HashMap<>(), false, false, true, false, false, 0, true);
59+
this(charset, locale, timeZone, maxRenderDepth, new HashMap<>(), false, false, true, false, false, 0, true, RandomNumberGeneratorStrategy.THREAD_LOCAL);
5860
}
5961

6062
private JinjavaConfig(Charset charset,
@@ -69,7 +71,8 @@ private JinjavaConfig(Charset charset,
6971
boolean enableRecursiveMacroCalls,
7072
boolean failOnUnknownTokens,
7173
long maxOutputSize,
72-
boolean nestedInterpretationEnabled) {
74+
boolean nestedInterpretationEnabled,
75+
RandomNumberGeneratorStrategy randomNumberGenerator) {
7376
this.charset = charset;
7477
this.locale = locale;
7578
this.timeZone = timeZone;
@@ -82,6 +85,7 @@ private JinjavaConfig(Charset charset,
8285
this.failOnUnknownTokens = failOnUnknownTokens;
8386
this.maxOutputSize = maxOutputSize;
8487
this.nestedInterpretationEnabled = nestedInterpretationEnabled;
88+
this.randomNumberGenerator = randomNumberGenerator;
8589
}
8690

8791
public Charset getCharset() {
@@ -104,6 +108,10 @@ public long getMaxOutputSize() {
104108
return maxOutputSize;
105109
}
106110

111+
public RandomNumberGeneratorStrategy getRandomNumberGeneratorStrategy() {
112+
return randomNumberGenerator;
113+
}
114+
107115
public boolean isTrimBlocks() {
108116
return trimBlocks;
109117
}
@@ -147,6 +155,7 @@ public static class Builder {
147155
private boolean enableRecursiveMacroCalls;
148156
private boolean failOnUnknownTokens;
149157
private boolean nestedInterpretationEnabled = true;
158+
private RandomNumberGeneratorStrategy randomNumberGeneratorStrategy = RandomNumberGeneratorStrategy.THREAD_LOCAL;
150159

151160
private Builder() {}
152161

@@ -175,6 +184,12 @@ public Builder withMaxRenderDepth(int maxRenderDepth) {
175184
return this;
176185
}
177186

187+
public Builder withRandomNumberGeneratorStrategy(RandomNumberGeneratorStrategy randomNumberGeneratorStrategy) {
188+
this.randomNumberGeneratorStrategy = randomNumberGeneratorStrategy;
189+
return this;
190+
}
191+
192+
178193
public Builder withTrimBlocks(boolean trimBlocks) {
179194
this.trimBlocks = trimBlocks;
180195
return this;
@@ -211,7 +226,7 @@ public Builder withNestedInterpretationEnabled(boolean nestedInterpretationEnabl
211226
}
212227

213228
public JinjavaConfig build() {
214-
return new JinjavaConfig(charset, locale, timeZone, maxRenderDepth, disabled, trimBlocks, lstripBlocks, readOnlyResolver, enableRecursiveMacroCalls, failOnUnknownTokens, maxOutputSize, nestedInterpretationEnabled);
229+
return new JinjavaConfig(charset, locale, timeZone, maxRenderDepth, disabled, trimBlocks, lstripBlocks, readOnlyResolver, enableRecursiveMacroCalls, failOnUnknownTokens, maxOutputSize, nestedInterpretationEnabled, randomNumberGeneratorStrategy);
215230
}
216231

217232
}

src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
import java.util.Map;
2626
import java.util.Objects;
2727
import java.util.Optional;
28+
import java.util.Random;
2829
import java.util.Set;
2930
import java.util.Stack;
31+
import java.util.concurrent.ThreadLocalRandom;
3032

3133
import org.apache.commons.lang3.StringUtils;
3234

@@ -36,6 +38,8 @@
3638
import com.hubspot.jinjava.Jinjava;
3739
import com.hubspot.jinjava.JinjavaConfig;
3840
import com.hubspot.jinjava.el.ExpressionResolver;
41+
import com.hubspot.jinjava.random.ConstantZeroRandomNumberGenerator;
42+
import com.hubspot.jinjava.random.RandomNumberGeneratorStrategy;
3943
import com.hubspot.jinjava.tree.Node;
4044
import com.hubspot.jinjava.tree.TreeParser;
4145
import com.hubspot.jinjava.tree.output.BlockPlaceholderOutputNode;
@@ -54,6 +58,7 @@ public class JinjavaInterpreter {
5458

5559
private final ExpressionResolver expressionResolver;
5660
private final Jinjava application;
61+
private final Random random;
5762

5863
private int lineNumber = -1;
5964
private final List<TemplateError> errors = new LinkedList<>();
@@ -63,6 +68,14 @@ public JinjavaInterpreter(Jinjava application, Context context, JinjavaConfig re
6368
this.config = renderConfig;
6469
this.application = application;
6570

71+
if (config.getRandomNumberGeneratorStrategy() == RandomNumberGeneratorStrategy.THREAD_LOCAL) {
72+
random = ThreadLocalRandom.current();
73+
} else if (config.getRandomNumberGeneratorStrategy() == RandomNumberGeneratorStrategy.CONSTANT_ZERO) {
74+
random = new ConstantZeroRandomNumberGenerator();
75+
} else {
76+
throw new IllegalStateException("No random number generator with strategy " + config.getRandomNumberGeneratorStrategy());
77+
}
78+
6679
this.expressionResolver = new ExpressionResolver(this, application.getExpressionFactory());
6780
}
6881

@@ -116,6 +129,10 @@ public void leaveScope() {
116129
}
117130
}
118131

132+
public Random getRandom() {
133+
return random;
134+
}
135+
119136
public class InterpreterScopeClosable implements AutoCloseable {
120137

121138
@Override

src/main/java/com/hubspot/jinjava/lib/filter/RandomFilter.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.Collection;
2121
import java.util.Iterator;
2222
import java.util.Map;
23-
import java.util.concurrent.ThreadLocalRandom;
2423

2524
import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
2625
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
@@ -52,7 +51,7 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar
5251
if (size == 0) {
5352
return null;
5453
}
55-
int index = ThreadLocalRandom.current().nextInt(size);
54+
int index = interpreter.getRandom().nextInt(size);
5655
while (index-- > 0) {
5756
it.next();
5857
}
@@ -64,7 +63,7 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar
6463
if (size == 0) {
6564
return null;
6665
}
67-
int index = ThreadLocalRandom.current().nextInt(size);
66+
int index = interpreter.getRandom().nextInt(size);
6867
return Array.get(object, index);
6968
}
7069
// map
@@ -75,20 +74,20 @@ public Object filter(Object object, JinjavaInterpreter interpreter, String... ar
7574
if (size == 0) {
7675
return null;
7776
}
78-
int index = ThreadLocalRandom.current().nextInt(size);
77+
int index = interpreter.getRandom().nextInt(size);
7978
while (index-- > 0) {
8079
it.next();
8180
}
8281
return it.next();
8382
}
8483
// number
8584
if (object instanceof Number) {
86-
return ThreadLocalRandom.current().nextLong(((Number) object).longValue());
85+
return interpreter.getRandom().nextInt(((Number) object).intValue());
8786
}
8887
// string
8988
if (object instanceof String) {
9089
try {
91-
return ThreadLocalRandom.current().nextLong(new BigDecimal((String) object).longValue());
90+
return interpreter.getRandom().nextInt(new BigDecimal((String) object).intValue());
9291
} catch (Exception e) {
9392
return 0;
9493
}

src/main/java/com/hubspot/jinjava/lib/filter/ShuffleFilter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ public String getName() {
2929
@Override
3030
public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
3131
if (var instanceof Collection) {
32-
List<?> list = new ArrayList<Object>((Collection<Object>) var);
33-
Collections.shuffle(list);
32+
List<?> list = new ArrayList<>((Collection<Object>) var);
33+
Collections.shuffle(list, interpreter.getRandom());
3434
return list;
3535
}
3636

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.hubspot.jinjava.random;
2+
3+
import java.util.Random;
4+
import java.util.stream.DoubleStream;
5+
import java.util.stream.IntStream;
6+
import java.util.stream.LongStream;
7+
8+
/**
9+
* A random number generator that always returns 0. Useful for testing code when you want the output to be constant.
10+
*/
11+
public class ConstantZeroRandomNumberGenerator extends Random {
12+
13+
@Override
14+
protected int next(int bits) {
15+
return 0;
16+
}
17+
18+
@Override
19+
public int nextInt() {
20+
return 0;
21+
}
22+
23+
@Override
24+
public int nextInt(int bound) {
25+
return 0;
26+
}
27+
28+
@Override
29+
public long nextLong() {
30+
return 0;
31+
}
32+
33+
@Override
34+
public boolean nextBoolean() {
35+
return false;
36+
}
37+
38+
@Override
39+
public float nextFloat() {
40+
return 0f;
41+
}
42+
43+
@Override
44+
public double nextDouble() {
45+
return 0;
46+
}
47+
48+
@Override
49+
public synchronized double nextGaussian() {
50+
return 0;
51+
}
52+
53+
@Override
54+
public void nextBytes(byte[] bytes) {
55+
throw new UnsupportedOperationException();
56+
}
57+
58+
@Override
59+
public IntStream ints(long streamSize) {
60+
throw new UnsupportedOperationException();
61+
}
62+
63+
@Override
64+
public IntStream ints() {
65+
throw new UnsupportedOperationException();
66+
}
67+
68+
@Override
69+
public IntStream ints(long streamSize, int randomNumberOrigin, int randomNumberBound) {
70+
throw new UnsupportedOperationException();
71+
}
72+
73+
@Override
74+
public IntStream ints(int randomNumberOrigin, int randomNumberBound) {
75+
throw new UnsupportedOperationException();
76+
}
77+
78+
@Override
79+
public LongStream longs(long streamSize) {
80+
throw new UnsupportedOperationException();
81+
}
82+
83+
@Override
84+
public LongStream longs() {
85+
throw new UnsupportedOperationException();
86+
}
87+
88+
@Override
89+
public LongStream longs(long streamSize, long randomNumberOrigin, long randomNumberBound) {
90+
throw new UnsupportedOperationException();
91+
}
92+
93+
@Override
94+
public LongStream longs(long randomNumberOrigin, long randomNumberBound) {
95+
throw new UnsupportedOperationException();
96+
}
97+
98+
@Override
99+
public DoubleStream doubles(long streamSize) {
100+
throw new UnsupportedOperationException();
101+
}
102+
103+
@Override
104+
public DoubleStream doubles() {
105+
throw new UnsupportedOperationException();
106+
}
107+
108+
@Override
109+
public DoubleStream doubles(long streamSize, double randomNumberOrigin, double randomNumberBound) {
110+
throw new UnsupportedOperationException();
111+
}
112+
113+
@Override
114+
public DoubleStream doubles(double randomNumberOrigin, double randomNumberBound) {
115+
throw new UnsupportedOperationException();
116+
}
117+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.hubspot.jinjava.random;
2+
3+
public enum RandomNumberGeneratorStrategy {
4+
THREAD_LOCAL,
5+
CONSTANT_ZERO
6+
}

src/test/java/com/hubspot/jinjava/lib/filter/ShuffleFilterTest.java

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,31 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
5+
import static org.mockito.Mockito.*;
56

67
import java.util.Arrays;
78
import java.util.List;
9+
import java.util.concurrent.ThreadLocalRandom;
810

9-
import org.junit.Before;
1011
import org.junit.Test;
1112

13+
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
14+
import com.hubspot.jinjava.random.ConstantZeroRandomNumberGenerator;
15+
1216
public class ShuffleFilterTest {
1317

14-
ShuffleFilter filter;
18+
ShuffleFilter filter = new ShuffleFilter();
1519

16-
@Before
17-
public void setup() {
18-
this.filter = new ShuffleFilter();
19-
}
20+
JinjavaInterpreter interpreter = mock(JinjavaInterpreter.class);
2021

2122
@SuppressWarnings("unchecked")
2223
@Test
23-
public void shuffleItems() {
24+
public void itShufflesItems() {
25+
26+
when(interpreter.getRandom()).thenReturn(ThreadLocalRandom.current());
27+
2428
List<String> before = Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9");
25-
List<String> after = (List<String>) filter.filter(before, null);
29+
List<String> after = (List<String>) filter.filter(before, interpreter);
2630

2731
assertThat(before).isSorted();
2832
assertThat(after).containsAll(before);
@@ -35,4 +39,19 @@ public void shuffleItems() {
3539
}
3640
}
3741

42+
@SuppressWarnings("unchecked")
43+
@Test
44+
public void itShufflesConsistentlyWithConstantRandom() {
45+
46+
when(interpreter.getRandom()).thenReturn(new ConstantZeroRandomNumberGenerator());
47+
48+
List<String> before = Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9");
49+
List<String> after = (List<String>) filter.filter(before, interpreter);
50+
51+
assertThat(before).isSorted();
52+
assertThat(after).containsAll(before);
53+
54+
assertThat(after).containsExactly("2", "3", "4", "5", "6", "7", "8", "9", "1");
55+
}
56+
3857
}

0 commit comments

Comments
 (0)