TemplateProcessor.java

1
package com.renomad.minum.templating;
2
3
4
import java.util.*;
5
import java.util.stream.Collectors;
6
7
/**
8
 * This class provides methods for working with templates.
9
 * <p>
10
 * The first step is to write a template.  Here is an example:
11
 * </p>
12
 * <pre>
13
 * Hello, my name is {{name}}
14
 * </pre>
15
 * <p>
16
 * Then, feed that string into the {@link #buildProcessor} method, like
17
 * this:
18
 * </p>
19
 * <pre>
20
 * {@code
21
 *   String input = "Hello, my name is {{name}}"
22
 *   TemplateProcessor helloProcessor = TemplateProcessor.buildProcessor(input);
23
 * }
24
 * </pre>
25
 * <p>
26
 * The returned value ("helloProcessor") can be rendered with different values. For
27
 * example:
28
 * </p>
29
 * <pre>
30
 * {@code
31
 *   Map<String,String> myMap = Map.of("name", "Susanne");
32
 *   String fullyRenderedString = helloProcessor.renderTemplate(myMap);
33
 * }
34
 * </pre>
35
 * <p>
36
 *     The result is:
37
 * </p>
38
 * <pre>
39
 *     {@code Hello, my name is Susanne}
40
 * </pre>
41
 */
42
public final class TemplateProcessor {
43
44
    /**
45
     * The template is made up of a list of these, which are the static (unchanging)
46
     * parts, along with the dynamic parts the user fills in later.
47
     */
48
    private final List<TemplateSection> templateSections;
49
50
    /**
51
     * This value is used to calculate a quick estimate of how many
52
     * bytes of memory we will need for the buffer holding our generated string
53
     */
54
    private final static double SIZE_ESTIMATE_MODIFIER = 1.1;
55
56
    /**
57
     * The String value given to us by the developer when creating this template
58
     */
59
    private final String originalText;
60
61
    /**
62
     * A general estimate of the resulting size of the output from the
63
     * template process, for one rendering of the template, to provide
64
     * quick information when building a buffer to contain the results
65
     * which is appropriately sized, which greatly improves performance
66
     */
67
    private final int estimatedSizeOfSingleTemplate;
68
69
    /**
70
     * Used when checking correctness - we check that the user is providing
71
     * key->value pairs that match the expected template keys.
72
     */
73
    private final Set<String> keysFoundInTemplate;
74
75
76
    /**
77
     * Instantiate a new object with a list of {@link TemplateSection}.
78
     */
79
    private TemplateProcessor(List<TemplateSection> templateSections, String originalText) {
80
        this.templateSections = templateSections;
81
        this.originalText = originalText;
82 1 1. <init> : Replaced double multiplication with division → TIMED_OUT
        this.estimatedSizeOfSingleTemplate = (int) (originalText.length() * SIZE_ESTIMATE_MODIFIER);
83
        keysFoundInTemplate = templateSections.stream()
84 2 1. lambda$new$0 : replaced boolean return with false for com/renomad/minum/templating/TemplateProcessor::lambda$new$0 → KILLED
2. lambda$new$0 : replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::lambda$new$0 → KILLED
                .filter(x -> x.templateType.equals(TemplateType.DYNAMIC_TEXT))
85 1 1. lambda$new$1 : replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::lambda$new$1 → KILLED
                .map(x -> x.key)
86
                .collect(Collectors.toUnmodifiableSet());
87
    }
88
89
    /**
90
     * Given a map of key names -> value, render a template.
91
     */
92
    public String renderTemplate(Map<String, String> myMap) {
93 1 1. renderTemplate : replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::renderTemplate → KILLED
        return renderTemplate(List.of(myMap), "");
94
    }
95
96
    /**
97
     * Given a list, map of key names -> value, render one template
98
     * for each, joined by a newline.  Use {@link #renderTemplate(List, String)}
99
     * for control over the delimiter.
100
     */
101
    public String renderTemplate(List<Map<String, String>> data) {
102 1 1. renderTemplate : replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::renderTemplate → KILLED
        return renderTemplate(data, "\n");
103
    }
104
105
    /**
106
     * Render a list of maps.
107
     * <p>
108
     *     Similar to {@link #renderTemplate(Map)} but takes a list of maps instead
109
     *     of just one.  The "delimiter" argument is inserted between each rendered
110
     *     template.
111
     * </p>
112
     */
113
    public String renderTemplate(List<Map<String, String>> data, String delimiter) {
114 1 1. renderTemplate : removed call to com/renomad/minum/templating/TemplateProcessor::correctnessCheck → KILLED
        correctnessCheck(data);
115
116
        // build an appropriately-sized buffer for output
117 1 1. renderTemplate : Replaced integer multiplication with division → SURVIVED
        int capacity = estimatedSizeOfSingleTemplate * data.size();
118
        StringBuilder stringBuilder = new StringBuilder(capacity);
119
120 2 1. renderTemplate : changed conditional boundary → KILLED
2. renderTemplate : negated conditional → KILLED
        for (int i = 0; i < data.size(); i++) {
121
            for (TemplateSection templateSection : templateSections) {
122 1 1. renderTemplate : removed call to com/renomad/minum/templating/TemplateSection::render → KILLED
                templateSection.render(data.get(i), stringBuilder);
123
            }
124 2 1. renderTemplate : Replaced integer subtraction with addition → KILLED
2. renderTemplate : negated conditional → KILLED
            if (i != data.size() - 1) {
125
                stringBuilder.append(delimiter);
126
            }
127
        }
128 1 1. renderTemplate : replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::renderTemplate → KILLED
        return stringBuilder.toString();
129
    }
130
131
132
    /**
133
     * Builds a {@link TemplateProcessor} from a string
134
     * containing a proper template.  Templated values
135
     * are surrounded by double-curly-braces, i.e. {{foo}} or {{ foo }}
136
     */
137
    public static TemplateProcessor buildProcessor(String template) {
138
        // this value holds the entire template after processing, comprised
139
        // of an ordered list of TemplateSections
140
        var tSections = new ArrayList<TemplateSection>();
141
142
        // these values are used for logging and setting proper indentation
143
        int rowNumber = 1;
144
        int columnNumber = 1;
145
146
        // this value records the indent of the beginning of template keys,
147
        // so we can properly indent the values later.
148
        int startOfKey = 0;
149
150
        StringBuilder builder = new StringBuilder();
151
152
        // this flag is to help us understand whether we are currently reading the
153
        // name of a template literal.
154
        // e.g. in the case of hello {{ name }}, "name" is the literal.
155
        boolean isInsideTemplateKeyLiteral = false;
156 2 1. buildProcessor : changed conditional boundary → KILLED
2. buildProcessor : negated conditional → KILLED
        for (int i = 0; i < template.length(); i++) {
157
            char charAtCursor = template.charAt(i);
158
159 1 1. buildProcessor : negated conditional → KILLED
            if (justArrivedInside(template, charAtCursor, i)) {
160
                isInsideTemplateKeyLiteral = true;
161
                startOfKey = columnNumber;
162 1 1. buildProcessor : Changed increment from 1 to -1 → TIMED_OUT
                i += 1;
163
                builder = processSectionInside(builder, tSections);
164 1 1. buildProcessor : negated conditional → KILLED
            } else if (justArrivedOutside(template, charAtCursor, i, isInsideTemplateKeyLiteral)) {
165
                isInsideTemplateKeyLiteral = false;
166 1 1. buildProcessor : Changed increment from 1 to -1 → KILLED
                i += 1;
167
                builder = processSectionOutside(builder, tSections, startOfKey);
168
                startOfKey = 0;
169
            } else {
170
                builder.append(charAtCursor);
171
172
                /*
173
                 if we're at the end of the template, it's our last chance to
174
                 add a substring (we can't be adding to a key, since if we're
175
                 at the end, and it's not a closing brace, it's a malformed
176
                 template.
177
                 */
178 2 1. buildProcessor : Replaced integer subtraction with addition → KILLED
2. buildProcessor : negated conditional → KILLED
                if (i == template.length() - 1) {
179 1 1. buildProcessor : negated conditional → KILLED
                    if (isInsideTemplateKeyLiteral) {
180
                        // if we're exiting this string while inside a template literal, then
181
                        // we're reading a corrupted input, and we should make that clear
182
                        // to our caller.
183 2 1. buildProcessor : negated conditional → KILLED
2. buildProcessor : changed conditional boundary → KILLED
                        String templateSample = template.length() > 10 ? template.substring(0, 10) + "..." : template;
184
                        throw new TemplateParseException(
185
                                "parsing failed for string starting with \"" + templateSample + "\" at line " + rowNumber + " and column " + columnNumber);
186
                    }
187
                    tSections.add(new TemplateSection(null, builder.toString(), 0, TemplateType.STATIC_TEXT));
188
                }
189
            }
190
191 1 1. buildProcessor : negated conditional → KILLED
            if (charAtCursor == '\n') {
192 1 1. buildProcessor : Changed increment from 1 to -1 → KILLED
                rowNumber += 1;
193
                columnNumber = 1;
194
            } else {
195 1 1. buildProcessor : Changed increment from 1 to -1 → KILLED
                columnNumber += 1;
196
            }
197
198
        }
199
200 1 1. buildProcessor : replaced return value with null for com/renomad/minum/templating/TemplateProcessor::buildProcessor → KILLED
        return new TemplateProcessor(tSections, template);
201
    }
202
203
    /**
204
     * Returns the raw template string provided at creation.
205
     */
206
    public String getOriginalText() {
207 1 1. getOriginalText : replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::getOriginalText → KILLED
        return this.originalText;
208
    }
209
210
    static StringBuilder processSectionInside(StringBuilder builder, List<TemplateSection> tSections) {
211 1 1. processSectionInside : negated conditional → KILLED
        if (!builder.isEmpty()) {
212
            tSections.add(new TemplateSection(null, builder.toString(), 0, TemplateType.STATIC_TEXT));
213
            builder = new StringBuilder();
214
        }
215 1 1. processSectionInside : replaced return value with null for com/renomad/minum/templating/TemplateProcessor::processSectionInside → KILLED
        return builder;
216
    }
217
218
    static StringBuilder processSectionOutside(StringBuilder builder, List<TemplateSection> tSections, int indent) {
219 1 1. processSectionOutside : negated conditional → KILLED
        if (!builder.isEmpty()) {
220
            String trimmedKey = builder.toString().trim();
221
            tSections.add(new TemplateSection(trimmedKey, null, indent, TemplateType.DYNAMIC_TEXT));
222
            builder = new StringBuilder();
223
        }
224 1 1. processSectionOutside : replaced return value with null for com/renomad/minum/templating/TemplateProcessor::processSectionOutside → KILLED
        return builder;
225
    }
226
227
    /**
228
     * Just left a template key value.
229
     * <pre>
230
     *     hello {{ world }}
231
     *                ^
232
     *                +------Template key
233
     *
234
     * </pre>
235
     */
236
    static boolean justArrivedOutside(String template, char charAtCursor, int i, boolean isInsideTemplateKeyLiteral) {
237 8 1. justArrivedOutside : negated conditional → KILLED
2. justArrivedOutside : Replaced integer addition with subtraction → KILLED
3. justArrivedOutside : changed conditional boundary → KILLED
4. justArrivedOutside : negated conditional → KILLED
5. justArrivedOutside : Replaced integer addition with subtraction → KILLED
6. justArrivedOutside : negated conditional → KILLED
7. justArrivedOutside : replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::justArrivedOutside → KILLED
8. justArrivedOutside : negated conditional → KILLED
        return charAtCursor == '}' && (i + 1) < template.length() && template.charAt(i + 1) == '}' && isInsideTemplateKeyLiteral;
238
    }
239
240
    /**
241
     * Just arrived inside a template key value.
242
     * <pre>
243
     *     hello {{ world }}
244
     *                ^
245
     *                +------Template key
246
     *
247
     * </pre>
248
     */
249
    static boolean justArrivedInside(String template, char charAtCursor, int i) {
250 7 1. justArrivedInside : negated conditional → KILLED
2. justArrivedInside : Replaced integer addition with subtraction → KILLED
3. justArrivedInside : Replaced integer addition with subtraction → KILLED
4. justArrivedInside : negated conditional → KILLED
5. justArrivedInside : replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::justArrivedInside → KILLED
6. justArrivedInside : negated conditional → KILLED
7. justArrivedInside : changed conditional boundary → KILLED
        return charAtCursor == '{' && (i + 1) < template.length() && template.charAt(i + 1) == '{';
251
    }
252
253
    /**
254
     * This examines the currently registered data lists and template keys
255
     * and confirms they are aligned.  It will throw an exception if they
256
     * are not perfectly correlated.
257
     * <br>
258
     *
259
     */
260
    private void correctnessCheck(List<Map<String, String>> dataList) {
261
        HashSet<String> copyOfKeysInTemplate = new HashSet<>(keysFoundInTemplate);
262
263
        // check for inconsistencies between maps in the data list
264
        Set<String> keysInFirstMap = dataList.getFirst().keySet();
265
266
        for (Map<String, String> data : dataList) {
267 1 1. correctnessCheck : negated conditional → KILLED
            if (!data.keySet().equals(keysInFirstMap)) {
268
                Set<String> result = differenceBetweenSets(data.keySet(), keysInFirstMap);
269
                throw new TemplateRenderException("In registered data, the maps were inconsistent on these keys: " + result);
270
            }
271
        }
272
273
        // ensure consistency between the registered data and the template keys
274
        HashSet<String> copyOfTemplateKeys = new HashSet<>(copyOfKeysInTemplate);
275
        copyOfTemplateKeys.removeAll(keysInFirstMap);
276 1 1. correctnessCheck : negated conditional → KILLED
        if (!copyOfTemplateKeys.isEmpty()) {
277
            throw new TemplateRenderException("These keys in the template were not provided data: " + copyOfTemplateKeys);
278
        }
279
280
        HashSet<String> copyOfDataKeys = new HashSet<>(keysInFirstMap);
281
        copyOfDataKeys.removeAll(copyOfKeysInTemplate);
282 1 1. correctnessCheck : negated conditional → KILLED
        if (!copyOfDataKeys.isEmpty()) {
283
            throw new TemplateRenderException("These keys in the data did not match anything in the template: " + copyOfDataKeys);
284
        }
285
    }
286
287
    private static Set<String> differenceBetweenSets(Set<String> set1, Set<String> set2) {
288
        Set<String> union = new HashSet<>(set2);
289
        union.addAll(set1);
290
        Set<String> intersection = new HashSet<>(set2);
291
        intersection.retainAll(set1);
292
293
        Set<String> result = new HashSet<>(union);
294
        result.removeAll(intersection);
295 1 1. differenceBetweenSets : replaced return value with Collections.emptySet for com/renomad/minum/templating/TemplateProcessor::differenceBetweenSets → KILLED
        return result;
296
    }
297
298
}
299

Mutations

82

1.1
Location : <init>
Killed by : none
Replaced double multiplication with division → TIMED_OUT

84

1.1
Location : lambda$new$0
Killed by : com.renomad.minum.templating.TemplatingTests
replaced boolean return with false for com/renomad/minum/templating/TemplateProcessor::lambda$new$0 → KILLED

2.2
Location : lambda$new$0
Killed by : com.renomad.minum.templating.TemplatingTests
replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::lambda$new$0 → KILLED

85

1.1
Location : lambda$new$1
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::lambda$new$1 → KILLED

93

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::renderTemplate → KILLED

102

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::renderTemplate → KILLED

114

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
removed call to com/renomad/minum/templating/TemplateProcessor::correctnessCheck → KILLED

117

1.1
Location : renderTemplate
Killed by : none
Replaced integer multiplication with division → SURVIVED
Covering tests

120

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
changed conditional boundary → KILLED

2.2
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

122

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
removed call to com/renomad/minum/templating/TemplateSection::render → KILLED

124

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
Replaced integer subtraction with addition → KILLED

2.2
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

128

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::renderTemplate → KILLED

156

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
changed conditional boundary → KILLED

2.2
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

159

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

162

1.1
Location : buildProcessor
Killed by : none
Changed increment from 1 to -1 → TIMED_OUT

164

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

166

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
Changed increment from 1 to -1 → KILLED

178

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
Replaced integer subtraction with addition → KILLED

2.2
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

179

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

183

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

2.2
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
changed conditional boundary → KILLED

191

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

192

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
Changed increment from 1 to -1 → KILLED

195

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
Changed increment from 1 to -1 → KILLED

200

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with null for com/renomad/minum/templating/TemplateProcessor::buildProcessor → KILLED

207

1.1
Location : getOriginalText
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::getOriginalText → KILLED

211

1.1
Location : processSectionInside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

215

1.1
Location : processSectionInside
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with null for com/renomad/minum/templating/TemplateProcessor::processSectionInside → KILLED

219

1.1
Location : processSectionOutside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_processSectionOutside_builderEmpty(com.renomad.minum.templating.TemplateSectionTests)
negated conditional → KILLED

224

1.1
Location : processSectionOutside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_processSectionOutside_builderEmpty(com.renomad.minum.templating.TemplateSectionTests)
replaced return value with null for com/renomad/minum/templating/TemplateProcessor::processSectionOutside → KILLED

237

1.1
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

2.2
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
Replaced integer addition with subtraction → KILLED

3.3
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
changed conditional boundary → KILLED

4.4
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

5.5
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
Replaced integer addition with subtraction → KILLED

6.6
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

7.7
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::justArrivedOutside → KILLED

8.8
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

250

1.1
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
negated conditional → KILLED

2.2
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
Replaced integer addition with subtraction → KILLED

3.3
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
Replaced integer addition with subtraction → KILLED

4.4
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
negated conditional → KILLED

5.5
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::justArrivedInside → KILLED

6.6
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
negated conditional → KILLED

7.7
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
changed conditional boundary → KILLED

267

1.1
Location : correctnessCheck
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

276

1.1
Location : correctnessCheck
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

282

1.1
Location : correctnessCheck
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

295

1.1
Location : differenceBetweenSets
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with Collections.emptySet for com/renomad/minum/templating/TemplateProcessor::differenceBetweenSets → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0