├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── src ├── main │ └── java │ │ ├── module-info.java │ │ └── com │ │ └── samskivert │ │ └── mustache │ │ ├── MustacheParseException.java │ │ ├── MustacheException.java │ │ ├── Escapers.java │ │ ├── DefaultCollector.java │ │ ├── BasicCollector.java │ │ └── Template.java └── test │ ├── resources │ └── custom │ │ └── specs │ │ ├── ~inheritance.yml │ │ ├── sections.yml │ │ └── partials.yml │ └── java │ └── com │ └── samskivert │ └── mustache │ ├── specs │ ├── CustomSpecTest.java │ ├── OfficialSpecTest.java │ ├── SpecAwareTemplateLoader.java │ ├── Spec.java │ └── SpecTest.java │ ├── ThreadSafetyTest.java │ ├── MustacheTest.java │ └── SharedTests.java ├── .gitmodules ├── .github ├── dependabot.yml └── workflows │ ├── test-report.yml │ └── maven.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── mvnw.cmd ├── pom.xml ├── mvnw └── README.md /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samskivert/jmustache/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.samskivert.jmustache { 2 | requires java.base; 3 | 4 | exports com.samskivert.mustache; 5 | } 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/test/resources/specs"] 2 | path = src/test/resources/specs 3 | url = https://github.com/mustache/spec.git 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | target 5 | .metadata 6 | 7 | # jenv version set for a local user 8 | .java-version 9 | 10 | # Mac OS X indexing meta data 11 | .DS_Store 12 | 13 | # IntelliJ developer local config 14 | .idea/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: java 4 | 5 | jdk: 6 | - openjdk9 7 | 8 | cache: 9 | directories: 10 | - '$HOME/.m2/repository' 11 | 12 | script: 13 | - mvn test -B 14 | # - mvn -B cobertura:cobertura coveralls:cobertura 15 | - rm -rf $HOME/.m2/repository/com/samskivert/jmustache 16 | -------------------------------------------------------------------------------- /.github/workflows/test-report.yml: -------------------------------------------------------------------------------- 1 | name: 'Test Report' 2 | on: 3 | workflow_run: 4 | workflows: ['Build with Maven'] 5 | types: 6 | - completed 7 | jobs: 8 | report: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: dorny/test-reporter@v1 12 | with: 13 | artifact: test-results 14 | name: Maven Surefire Tests 15 | path: 'TEST-*.xml' 16 | reporter: java-junit 17 | -------------------------------------------------------------------------------- /src/test/resources/custom/specs/~inheritance.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Custom inheritance tests. 3 | tests: 4 | - name: Non block content should not impact standalone blocks 5 | desc: Content inside a parent call that is not block tags should be ignored 6 | data: { } 7 | template: "{{ data () { 18 | String[] groups = new String[] { 19 | "sections", 20 | "partials", 21 | "~inheritance" 22 | }; 23 | return SpecTest.data("/custom/specs/", groups); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/samskivert/mustache/specs/OfficialSpecTest.java: -------------------------------------------------------------------------------- 1 | package com.samskivert.mustache.specs; 2 | 3 | import java.util.Collection; 4 | 5 | import org.junit.runner.RunWith; 6 | import org.junit.runners.Parameterized; 7 | import org.junit.runners.Parameterized.Parameters; 8 | 9 | @RunWith(Parameterized.class) 10 | public class OfficialSpecTest extends SpecTest { 11 | 12 | public OfficialSpecTest(Spec spec, String name) { 13 | super(spec, name); 14 | } 15 | 16 | @Parameters(name = "{1}") 17 | public static Collection data () { 18 | String[] groups = new String[] { 19 | "comments", 20 | "delimiters", 21 | "interpolation", 22 | "inverted", 23 | "sections", 24 | "partials", 25 | "~inheritance" 26 | }; 27 | return SpecTest.data("/specs/specs/", groups); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/samskivert/mustache/specs/SpecAwareTemplateLoader.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache.specs; 6 | 7 | import java.io.Reader; 8 | import java.io.StringReader; 9 | 10 | import com.samskivert.mustache.Mustache; 11 | 12 | public class SpecAwareTemplateLoader implements Mustache.TemplateLoader 13 | { 14 | private static final String EMPTY_STRING = ""; 15 | private final Spec spec; 16 | 17 | public SpecAwareTemplateLoader(Spec spec) { 18 | super(); 19 | this.spec = spec; 20 | } 21 | 22 | @Override public Reader getTemplate (String name) throws Exception { 23 | if (spec == null) return new StringReader(EMPTY_STRING); 24 | String partial = spec.getPartial(name); 25 | return new StringReader(partial == null ? EMPTY_STRING : partial); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar 19 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Build with Maven 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | 13 | strategy: 14 | matrix: 15 | java: ['17', '21'] 16 | 17 | runs-on: ubuntu-latest 18 | env: 19 | BUILD_NUMBER: "${{github.run_number}}" 20 | MAVEN_CLI_OPTS: "--batch-mode --no-transfer-progress" 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | submodules: 'true' 26 | - name: Set up JDK ${{ matrix.java }} 27 | uses: actions/setup-java@v3 28 | with: 29 | java-version: ${{ matrix.java }} 30 | distribution: 'temurin' 31 | cache: 'maven' 32 | - name: Build and Test with Maven 33 | run: mvn $MAVEN_CLI_OPTS clean verify 34 | - name: Upload Test Results 35 | uses: actions/upload-artifact@v4 36 | if: ${{ always() && matrix.java == '17' }} 37 | with: 38 | name: test-results 39 | path: 'target/surefire-reports/TEST-*.xml' 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Michael Bayne 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * The name Michael Bayne may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /src/main/java/com/samskivert/mustache/MustacheException.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache; 6 | 7 | /** 8 | * An exception thrown when an error occurs parsing or executing a Mustache template. 9 | */ 10 | public class MustacheException extends RuntimeException 11 | { 12 | /** An exception thrown if we encounter a context error (e.g. a missing variable) while 13 | * compiling or executing a template. */ 14 | public static class Context extends MustacheException { 15 | /** The key that caused the problem. */ 16 | public final String key; 17 | 18 | /** The line number of the template on which the problem occurred. */ 19 | public final int lineNo; 20 | 21 | public Context (String message, String key, int lineNo) { 22 | super(message); 23 | this.key = key; 24 | this.lineNo = lineNo; 25 | } 26 | 27 | public Context (String message, String key, int lineNo, Throwable cause) { 28 | super(message, cause); 29 | this.key = key; 30 | this.lineNo = lineNo; 31 | } 32 | } 33 | 34 | public MustacheException (String message) { 35 | super(message); 36 | } 37 | 38 | public MustacheException (Throwable cause) { 39 | super(cause); 40 | } 41 | 42 | public MustacheException (String message, Throwable cause) { 43 | super(message, cause); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/samskivert/mustache/specs/Spec.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache.specs; 6 | 7 | import java.util.Collections; 8 | import java.util.Map; 9 | import java.util.Map.Entry; 10 | import java.util.function.Consumer; 11 | 12 | /** 13 | * @author Yoryos Valotasios 14 | */ 15 | public class Spec 16 | { 17 | private final Map map; 18 | private final Map partials; 19 | 20 | public Spec (Map map) { 21 | this.map = map; 22 | @SuppressWarnings("unchecked") Map partials = 23 | (Map) map.get("partials"); 24 | if (partials == null) partials = Collections.emptyMap(); 25 | this.partials = partials; 26 | } 27 | 28 | public String getName () { 29 | return (String) map.get("name"); 30 | } 31 | 32 | public String getDescription () { 33 | return (String) map.get("desc"); 34 | } 35 | 36 | public String getTemplate () { 37 | return (String) map.get("template"); 38 | } 39 | 40 | public String getExpectedOutput () { 41 | return (String) map.get("expected"); 42 | } 43 | 44 | public Object getData () { 45 | return map.get("data"); 46 | } 47 | 48 | public String getPartial (String name) { 49 | return partials == null ? null : partials.get(name); 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | StringBuilder sb = new StringBuilder(); 55 | Consumer value = s -> sb.append("\"").append(s).append("\"").append("\n"); 56 | Consumer label = s -> sb.append("").append(s).append(": "); 57 | label.accept("name"); 58 | value.accept(getName()); 59 | label.accept("desc"); 60 | value.accept(getDescription()); 61 | label.accept("template"); 62 | value.accept(getTemplate()); 63 | if (! partials.isEmpty()) { 64 | label.accept("partials"); 65 | sb.append("\n"); 66 | for( Entry e : partials.entrySet()) { 67 | sb.append("\t").append(e.getKey()).append(":\n"); 68 | value.accept(e.getValue()); 69 | } 70 | sb.append("\n"); 71 | } 72 | label.accept("expected"); 73 | value.accept(getExpectedOutput()); 74 | return sb.toString(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/resources/custom/specs/partials.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Partial tags are used to expand an external template into the current 3 | template. 4 | 5 | The tag's content MUST be a non-whitespace character sequence NOT containing 6 | the current closing delimiter. 7 | 8 | This tag's content names the partial to inject. Set Delimiter tags MUST NOT 9 | affect the parsing of a partial. The partial MUST be rendered against the 10 | context stack local to the tag. If the named partial cannot be found, the 11 | empty string SHOULD be used instead, as in interpolations. 12 | 13 | Partial tags SHOULD be treated as standalone when appropriate. If this tag 14 | is used standalone, any whitespace preceding the tag should treated as 15 | indentation, and prepended to each line of the partial before rendering. 16 | tests: 17 | 18 | - name: Nested Partial Indent 19 | desc: "Nested partials should including the parent partials indent" 20 | data: { content: "<\n->" } 21 | template: "|\n {{>partial}}\n|\n" 22 | partials: 23 | partial: "1\n {{>nest}}\n1\n" 24 | nest: "2\n{{{content}}}\n2\n" 25 | expected: "|\n 1\n 2\n <\n->\n 2\n 1\n|\n" 26 | 27 | - name: Partial Section Indentation End Content 28 | desc: Closing end sections that have content on same line should be indented 29 | data: { content: "<\n->" } 30 | template: | 31 | \ 32 | {{>partial}} 33 | / 34 | partials: 35 | partial: | 36 | | 37 | {{#content}} 38 | {{{.}}} 39 | {{/content}}- 40 | | 41 | expected: | 42 | \ 43 | | 44 | < 45 | -> 46 | - 47 | | 48 | / 49 | 50 | - name: Partial Section Indentation Inside Start Content 51 | desc: Content that is not white space on same line as section start tag inside should be indented 52 | data: { content: "<\n->" } 53 | template: | 54 | \ 55 | {{>partial}} 56 | / 57 | partials: 58 | partial: | 59 | | 60 | {{#content}}- 61 | {{{.}}} 62 | {{/content}} 63 | | 64 | expected: | 65 | \ 66 | | 67 | - 68 | < 69 | -> 70 | | 71 | / 72 | 73 | - name: Partial Section Indentation Start Content 74 | desc: Content that is not white space on same line as section start tags should be indented 75 | data: { content: "<\n->" } 76 | template: | 77 | \ 78 | {{>partial}} 79 | / 80 | partials: 81 | partial: | 82 | | 83 | -{{#content}} 84 | {{{.}}} 85 | {{/content}} 86 | | 87 | expected: | 88 | \ 89 | | 90 | - 91 | < 92 | -> 93 | | 94 | / 95 | 96 | - name: Partial Indentation With Empty Sections 97 | desc: Empty standlone sections should not have indentation before or after 98 | data: { content: "" } 99 | template: | 100 | \ 101 | {{>partial}} 102 | / 103 | partials: 104 | partial: | 105 | | 106 | {{#content}} 107 | {{{.}}} 108 | {{/content}} 109 | | 110 | expected: | 111 | \ 112 | | 113 | | 114 | / 115 | 116 | # The extra newline on the end is required -------------------------------------------------------------------------------- /src/test/java/com/samskivert/mustache/specs/SpecTest.java: -------------------------------------------------------------------------------- 1 | package com.samskivert.mustache.specs; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | 9 | import org.junit.Assert; 10 | import org.junit.Test; 11 | import org.yaml.snakeyaml.Yaml; 12 | import org.yaml.snakeyaml.error.YAMLException; 13 | 14 | import com.samskivert.mustache.Mustache; 15 | import com.samskivert.mustache.Template; 16 | 17 | public abstract class SpecTest { 18 | 19 | private static final Yaml yaml = new Yaml(); 20 | 21 | private final Spec spec; 22 | private final String name; 23 | 24 | public SpecTest (Spec spec, String name) { 25 | super(); 26 | this.spec = spec; 27 | this.name = name; 28 | } 29 | 30 | @Test 31 | public void test () throws Exception { 32 | //System.out.println("Testing: " + name); 33 | SpecAwareTemplateLoader loader = new SpecAwareTemplateLoader(spec); 34 | Mustache.Compiler compiler = Mustache.compiler().emptyStringIsFalse(true).defaultValue("").withLoader(loader); 35 | String tmpl = spec.getTemplate(); 36 | String desc = String.format("Template: '''%s'''\nData: '%s'\n", 37 | uncrlf(tmpl), uncrlf(spec.getData().toString())); 38 | try { 39 | Template t = compiler.compile(spec.getTemplate()); 40 | String out = t.execute(spec.getData()); 41 | if (! Objects.equals(uncrlf(spec.getExpectedOutput()), uncrlf(out))) { 42 | System.out.println(""); 43 | System.out.println("----------------------------------------"); 44 | System.out.println(""); 45 | System.out.println("Failed: " + name); 46 | System.out.println(spec); 47 | System.out.println("Expected : \"" + showWhitespace(spec.getExpectedOutput()) + "\""); 48 | System.out.println("Result : \"" + showWhitespace(out) + "\""); 49 | System.out.println("----------------------------------------"); 50 | System.out.println(""); 51 | } 52 | Assert.assertEquals(desc, showWhitespace(spec.getExpectedOutput()), showWhitespace(out)); 53 | } catch (Exception e) { 54 | 55 | // the specs tests assume that the engine silently ignores invalid delimiter 56 | // specifications, but we throw an exception (and rightfully so IMO; this is not a 57 | // place where silent failure is helpful), so just ignore those test failures 58 | if (!e.getMessage().contains("Invalid delimiter")) { 59 | e.printStackTrace(); 60 | Assert.fail( 61 | desc + "\nExpected: " + uncrlf(spec.getExpectedOutput()) + "\nError: " + e); 62 | } 63 | } 64 | } 65 | 66 | public static String showWhitespace (String s) { 67 | s = s.replace("\r\n", "\u240D"); 68 | s = s.replace('\t', '\u21E5'); 69 | s = s.replace("\n", "\u21B5\n"); 70 | s = s.replace("\u240D", "\u240D\n"); 71 | return s; 72 | } 73 | 74 | private static String uncrlf (String text) { 75 | return (text == null) ? null : text.replace("\r", "\\r").replace("\n", "\\n"); 76 | } 77 | 78 | public static Collection data (String specPath, String[] groups) { 79 | List tuples = new ArrayList<>(); 80 | int i = 0; 81 | for (String g : groups) { 82 | Iterable specs = getTestsForGroup(specPath, g); 83 | for (Spec s : specs) { 84 | Object[] tuple = new Object[] {s, g + "-" + s.getName() + "-" + i++}; 85 | tuples.add(tuple); 86 | } 87 | } 88 | return tuples; 89 | } 90 | 91 | private static Iterable getTestsForGroup (String specPath, String name) { 92 | //String ymlPath = "/specs/specs/" + name + ".yml"; 93 | String ymlPath = specPath + name + ".yml"; 94 | return getTestsFromYaml(name, ymlPath); 95 | } 96 | 97 | private static Iterable getTestsFromYaml(String name, String ymlPath) { 98 | try { 99 | @SuppressWarnings("unchecked") 100 | Map map = (Map) yaml.load(SpecTest.class.getResourceAsStream(ymlPath)); 101 | @SuppressWarnings("unchecked") 102 | List> tests = (List>) map.get("tests"); 103 | List specs = new ArrayList<>(); 104 | for (Map t : tests) { 105 | specs.add(new Spec(t)); 106 | } 107 | return specs; 108 | } catch (YAMLException err) { 109 | System.err.println("*** Error loading: " + ymlPath); 110 | System.err.println("*** You probably need to 'git submodule update'."); 111 | throw err; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/test/java/com/samskivert/mustache/ThreadSafetyTest.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache; 6 | 7 | import java.io.IOException; 8 | import java.io.Reader; 9 | import java.io.StringReader; 10 | import java.util.HashMap; 11 | import java.util.LinkedList; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | import java.util.concurrent.ConcurrentLinkedDeque; 16 | import java.util.concurrent.CyclicBarrier; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.concurrent.atomic.AtomicInteger; 21 | 22 | import com.samskivert.mustache.Mustache; 23 | import com.samskivert.mustache.Template; 24 | 25 | import org.junit.Test; 26 | import static org.junit.Assert.assertEquals; 27 | import static org.junit.Assert.assertTrue; 28 | import static org.junit.Assert.fail; 29 | 30 | public class ThreadSafetyTest { 31 | 32 | private static final String templateA = "Template A content\n{{> templateB}}"; 33 | private static final String templateB = "Template B content"; 34 | 35 | private static class CountingLoader implements Mustache.TemplateLoader { 36 | private final AtomicInteger counter = new AtomicInteger(0); 37 | 38 | @Override public Reader getTemplate (String name) throws Exception { 39 | counter.incrementAndGet(); 40 | if (!"templateB".equals(name)) { 41 | throw new IllegalArgumentException(); 42 | } 43 | return new StringReader(templateB); 44 | } 45 | 46 | public int counter () { 47 | return counter.get(); 48 | } 49 | } 50 | 51 | @Test 52 | public void testTemplateLoading () throws InterruptedException { 53 | // 4096 works but is disabled as it makes the test take a long time 54 | for (int ii : new int[] { 1, 2, 4, 8, 16, 32, 256, 1024/* , 4096 */ }) { 55 | templateLoadingIsThreadSafe(ii); 56 | } 57 | } 58 | 59 | void templateLoadingIsThreadSafe (int nThreads) throws InterruptedException { 60 | final CyclicBarrier barrier = new CyclicBarrier(nThreads); 61 | final CountingLoader loader = new CountingLoader(); 62 | final Mustache.Compiler compiler = Mustache.compiler().withLoader(loader); 63 | final Template template = compiler.compile(new StringReader(templateA)); 64 | 65 | final List threads = new LinkedList<>(); 66 | for (int i = 0; i < nThreads; i++) { 67 | final Thread thread = new Thread(() -> { 68 | try { 69 | barrier.await(); 70 | final String value = template.execute(Map.of()); 71 | if (!value.contains("Template A content\nTemplate B content")) { 72 | fail("Invalid template result " + value); 73 | } 74 | } catch (Exception e) { 75 | throw new RuntimeException(e); 76 | } 77 | }); 78 | 79 | threads.add(thread); 80 | thread.start(); 81 | } 82 | 83 | for (Thread thread : threads) { 84 | thread.join(); 85 | } 86 | 87 | assertEquals(1, loader.counter()); 88 | } 89 | 90 | @Test 91 | public void testPartialThreadSafe () throws Exception { 92 | AtomicInteger loadCount = new AtomicInteger(); 93 | Mustache.TemplateLoader loader = new Mustache.TemplateLoader() { 94 | @Override 95 | public Reader getTemplate(String name) throws Exception { 96 | if ("partial".equals(name)) { 97 | loadCount.incrementAndGet(); 98 | TimeUnit.MILLISECONDS.sleep(20); 99 | return new StringReader("Hello"); 100 | } 101 | throw new IOException(name); 102 | } 103 | }; 104 | 105 | Template template = Mustache.compiler().withLoader(loader). 106 | compile("{{stuff}}\n\t{{> partial }}"); 107 | ExecutorService executor = Executors.newFixedThreadPool(64); 108 | ConcurrentLinkedDeque q = new ConcurrentLinkedDeque<>(); 109 | 110 | Map m = new HashMap<>(); 111 | m.put("stuff", "Foo"); 112 | for (int i = 100; i > 0; i--) { 113 | int ii = i; 114 | executor.execute(() -> { 115 | try { 116 | TimeUnit.MILLISECONDS.sleep(ii % 10); 117 | template.execute(m); 118 | } catch (Exception e) { 119 | q.add(e); 120 | } 121 | }); 122 | } 123 | executor.shutdown(); 124 | executor.awaitTermination(10_000, TimeUnit.MILLISECONDS); 125 | if (!q.isEmpty()) { 126 | System.out.println(q); 127 | } 128 | assertTrue(q.isEmpty()); 129 | assertEquals(1, loadCount.get()); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/samskivert/mustache/Escapers.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache; 6 | 7 | import java.io.IOException; 8 | import java.io.UncheckedIOException; 9 | 10 | /** 11 | * Defines some standard {@link Mustache.Escaper}s. 12 | */ 13 | public class Escapers 14 | { 15 | // TODO for 2.0 this class should be final and have a private constructor but that 16 | // would break semver on the off chance someone did extend it. 17 | 18 | /** Escapes HTML entities. */ 19 | public static final Mustache.Escaper HTML = simple(new String[][] { 20 | { "&", "&" }, 21 | { "'", "'" }, 22 | { "\"", """ }, 23 | { "<", "<" }, 24 | { ">", ">" }, 25 | { "`", "`" }, 26 | { "=", "=" } 27 | }); 28 | 29 | /** An escaper that does no escaping. */ 30 | public static final Mustache.Escaper NONE = new Mustache.Escaper() { 31 | @Override public String escape (String text) { 32 | return text; 33 | } 34 | }; 35 | 36 | /** Returns an escaper that replaces a list of text sequences with canned replacements. 37 | * @param repls a list of {@code (text, replacement)} pairs. */ 38 | public static Mustache.Escaper simple (final String[]... repls) { 39 | String[] lookupTable = Lookup7bitEscaper.createTable(repls); 40 | if (lookupTable != null) { 41 | return new Lookup7bitEscaper(lookupTable); 42 | } 43 | // our lookup replacements are not 7 bit ascii. 44 | return new Mustache.Escaper() { 45 | @Override public String escape (String text) { 46 | for (String[] escape : repls) { 47 | text = text.replace(escape[0], escape[1]); 48 | } 49 | return text; 50 | } 51 | }; 52 | } 53 | // This is based on benchmarking: https://github.com/jstachio/escape-benchmark 54 | private static class Lookup7bitEscaper implements Mustache.Escaper { 55 | /* 56 | * This only works for replacing the lower 7 bit ascii 57 | * characters 58 | */ 59 | private final String[] lookupTable; 60 | 61 | private Lookup7bitEscaper( 62 | String[] lookupTable) { 63 | super(); 64 | this.lookupTable = lookupTable; 65 | } 66 | 67 | static /* @Nullable */ String[] createTable (String[][] mappings) { 68 | String[] table = new String[128]; 69 | for (String[] entry : mappings) { 70 | String key = entry[0]; 71 | String value = entry[1]; 72 | if (key.length() != 1) { 73 | return null; 74 | } 75 | char k = key.charAt(0); 76 | if (k > 127) { 77 | return null; 78 | } 79 | table[k] = value; 80 | } 81 | return table; 82 | } 83 | 84 | @Override 85 | public void escape (Appendable a, CharSequence raw) throws IOException { 86 | int end = raw.length(); 87 | for (int i = 0, start = 0; i < end; i++) { 88 | char c = raw.charAt(i); 89 | String found = escapeChar(lookupTable, c); 90 | /* 91 | * While this could be done with one loop it appears through 92 | * benchmarking that by having the first loop assume the string 93 | * to be not changed creates a fast path for strings with no escaping needed. 94 | */ 95 | if (found != null) { 96 | a.append(raw, 0, i); 97 | a.append(found); 98 | start = i = i + 1; 99 | for (; i < end; i++) { 100 | c = raw.charAt(i); 101 | found = escapeChar(lookupTable, c); 102 | if (found != null) { 103 | a.append(raw, start, i); 104 | a.append(found); 105 | start = i + 1; 106 | } 107 | } 108 | a.append(raw, start, end); 109 | return; 110 | } 111 | } 112 | a.append(raw); 113 | } 114 | 115 | private static /* @Nullable */ String escapeChar (String[] lookupTable, char c) { 116 | if (c > 127) { 117 | return null; 118 | } 119 | return lookupTable[c]; 120 | } 121 | 122 | @Override 123 | public String escape (String raw) { 124 | StringBuilder sb = new StringBuilder(raw.length()); 125 | try { 126 | escape(sb, raw); 127 | } catch (IOException e) { 128 | throw new UncheckedIOException(e); 129 | } 130 | return sb.toString(); 131 | } 132 | 133 | @Override 134 | public String toString () { 135 | StringBuilder sb = new StringBuilder(); 136 | sb.append("Escaper["); 137 | for(char i = 0; i < lookupTable.length; i++) { 138 | String value = lookupTable[i]; 139 | if (value != null) { 140 | sb.append("{'").append(i).append("', '").append(value).append("'},"); 141 | } 142 | } 143 | sb.append("]"); 144 | return sb.toString(); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/com/samskivert/mustache/DefaultCollector.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache; 6 | 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.Method; 9 | 10 | import java.util.LinkedHashSet; 11 | import java.util.Map; 12 | import java.util.Set; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | /** 16 | * The default collector used by JMustache. 17 | */ 18 | public class DefaultCollector extends BasicCollector 19 | { 20 | private final boolean _allowAccessCoercion; 21 | 22 | public DefaultCollector () { 23 | this(true); 24 | } 25 | 26 | public DefaultCollector (boolean allowAccessCoercion) { 27 | _allowAccessCoercion = allowAccessCoercion; 28 | } 29 | 30 | @Override 31 | public Mustache.VariableFetcher createFetcher (Object ctx, String name) { 32 | Mustache.VariableFetcher fetcher = super.createFetcher(ctx, name); 33 | if (fetcher != null) return fetcher; 34 | 35 | // first check for a getter which provides the value 36 | Class cclass = ctx.getClass(); 37 | final Method m = getMethod(cclass, name); 38 | if (m != null) { 39 | return new Mustache.VariableFetcher() { 40 | public Object get (Object ctx, String name) throws Exception { 41 | return m.invoke(ctx); 42 | } 43 | }; 44 | } 45 | 46 | // next check for a getter which provides the value 47 | final Field f = getField(cclass, name); 48 | if (f != null) { 49 | return new Mustache.VariableFetcher() { 50 | public Object get (Object ctx, String name) throws Exception { 51 | return f.get(ctx); 52 | } 53 | }; 54 | } 55 | 56 | // finally check for a default interface method which provides the value (this is left to 57 | // last because it's much more expensive and hopefully something already matched above) 58 | final Method im = getIfaceMethod(cclass, name); 59 | if (im != null) { 60 | return new Mustache.VariableFetcher() { 61 | public Object get (Object ctx, String name) throws Exception { 62 | return im.invoke(ctx); 63 | } 64 | }; 65 | } 66 | 67 | return null; 68 | } 69 | 70 | @Override 71 | public Map createFetcherCache () { 72 | return new ConcurrentHashMap(); 73 | } 74 | 75 | protected Method getMethod (Class clazz, String name) { 76 | if (_allowAccessCoercion) { 77 | // first check up the superclass chain 78 | for (Class cc = clazz; cc != null && cc != Object.class; cc = cc.getSuperclass()) { 79 | Method m = getMethodOn(cc, name); 80 | if (m != null) return m; 81 | } 82 | } else { 83 | // if we only allow access to accessible methods, then we can just let the JVM handle 84 | // searching superclasses for the method 85 | try { 86 | return clazz.getMethod(name); 87 | } catch (Exception e) { 88 | // fall through 89 | } 90 | } 91 | return null; 92 | } 93 | 94 | protected Method getIfaceMethod (Class clazz, String name) { 95 | // enumerate the transitive closure of all interfaces implemented by clazz 96 | Set> ifaces = new LinkedHashSet>(); 97 | for (Class cc = clazz; cc != null && cc != Object.class; cc = cc.getSuperclass()) { 98 | addIfaces(ifaces, cc, false); 99 | } 100 | // now search those in the order that we found them 101 | for (Class iface : ifaces) { 102 | Method m = getMethodOn(iface, name); 103 | if (m != null) return m; 104 | } 105 | return null; 106 | } 107 | 108 | private void addIfaces (Set> ifaces, Class clazz, boolean isIface) { 109 | if (isIface) ifaces.add(clazz); 110 | for (Class iface : clazz.getInterfaces()) addIfaces(ifaces, iface, true); 111 | } 112 | 113 | protected Method getMethodOn (Class clazz, String name) { 114 | Method m; 115 | try { 116 | m = clazz.getDeclaredMethod(name); 117 | if (!m.getReturnType().equals(void.class)) return makeAccessible(m); 118 | } catch (Exception e) { 119 | // fall through 120 | } 121 | 122 | String upperName = Character.toUpperCase(name.charAt(0)) + name.substring(1); 123 | try { 124 | m = clazz.getDeclaredMethod("get" + upperName); 125 | if (!m.getReturnType().equals(void.class)) return makeAccessible(m); 126 | } catch (Exception e) { 127 | // fall through 128 | } 129 | 130 | try { 131 | m = clazz.getDeclaredMethod("is" + upperName); 132 | if (m.getReturnType().equals(boolean.class) || 133 | m.getReturnType().equals(Boolean.class)) return makeAccessible(m); 134 | } catch (Exception e) { 135 | // fall through 136 | } 137 | 138 | return null; 139 | } 140 | 141 | private Method makeAccessible (Method m) { 142 | if (m.isAccessible()) return m; 143 | else if (!_allowAccessCoercion) return null; 144 | m.setAccessible(true); 145 | return m; 146 | } 147 | 148 | protected Field getField (Class clazz, String name) { 149 | if (!_allowAccessCoercion) { 150 | try { 151 | return clazz.getField(name); 152 | } catch (Exception e) { 153 | return null; 154 | } 155 | } 156 | 157 | Field f; 158 | try { 159 | f = clazz.getDeclaredField(name); 160 | if (!f.isAccessible()) { 161 | f.setAccessible(true); 162 | } 163 | return f; 164 | } catch (Exception e) { 165 | // fall through 166 | } 167 | 168 | Class sclass = clazz.getSuperclass(); 169 | if (sclass != Object.class && sclass != null) { 170 | return getField(clazz.getSuperclass(), name); 171 | } 172 | return null; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 168 | " exit 1;"^ 169 | "}"^ 170 | "}" 171 | if ERRORLEVEL 1 goto error 172 | ) 173 | 174 | @REM Provide a "standardized" way to retrieve the CLI args that will 175 | @REM work with both Windows and non-Windows executions. 176 | set MAVEN_CMD_LINE_ARGS=%* 177 | 178 | %MAVEN_JAVA_EXE% ^ 179 | %JVM_CONFIG_MAVEN_PROPS% ^ 180 | %MAVEN_OPTS% ^ 181 | %MAVEN_DEBUG_OPTS% ^ 182 | -classpath %WRAPPER_JAR% ^ 183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 185 | if ERRORLEVEL 1 goto error 186 | goto end 187 | 188 | :error 189 | set ERROR_CODE=1 190 | 191 | :end 192 | @endlocal & set ERROR_CODE=%ERROR_CODE% 193 | 194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 198 | :skipRcPost 199 | 200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 202 | 203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 204 | 205 | cmd /C exit /B %ERROR_CODE% 206 | -------------------------------------------------------------------------------- /src/main/java/com/samskivert/mustache/BasicCollector.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache; 6 | 7 | import java.util.Collections; 8 | import java.util.Iterator; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.NoSuchElementException; 12 | 13 | /** 14 | * A collector that does not use reflection and can be used with GWT. 15 | */ 16 | public abstract class BasicCollector implements Mustache.Collector 17 | { 18 | public Iterator toIterator (final Object value) { 19 | if (value instanceof Iterable) { 20 | return ((Iterable)value).iterator(); 21 | } 22 | if (value instanceof Iterator) { 23 | return (Iterator)value; 24 | } 25 | if (value.getClass().isArray()) { 26 | final ArrayHelper helper = arrayHelper(value); 27 | return new Iterator() { 28 | private int _count = helper.length(value), _idx; 29 | @Override public boolean hasNext () { return _idx < _count; } 30 | @Override public Object next () { return helper.get(value, _idx++); } 31 | @Override public void remove () { throw new UnsupportedOperationException(); } 32 | }; 33 | } 34 | return null; 35 | } 36 | 37 | public Mustache.VariableFetcher createFetcher (Object ctx, String name) { 38 | if (ctx instanceof Mustache.CustomContext) return CUSTOM_FETCHER; 39 | if (ctx instanceof Map) return MAP_FETCHER; 40 | 41 | // if the name looks like a number, potentially use one of our 'indexing' fetchers 42 | char c = name.charAt(0); 43 | if (c >= '0' && c <= '9') { 44 | if (ctx instanceof List) return LIST_FETCHER; 45 | if (ctx instanceof Iterator) return ITER_FETCHER; 46 | if (ctx.getClass().isArray()) return arrayHelper(ctx); 47 | } 48 | 49 | return null; 50 | } 51 | 52 | /** This should return a thread-safe map, either {@link Collections#synchronizedMap} called on 53 | * a standard {@link Map} implementation or something like {@code ConcurrentHashMap}. */ 54 | public abstract Map createFetcherCache (); 55 | 56 | protected static ArrayHelper arrayHelper (Object ctx) { 57 | if (ctx instanceof Object[]) return OBJECT_ARRAY_HELPER; 58 | if (ctx instanceof boolean[]) return BOOLEAN_ARRAY_HELPER; 59 | if (ctx instanceof byte[]) return BYTE_ARRAY_HELPER; 60 | if (ctx instanceof char[]) return CHAR_ARRAY_HELPER; 61 | if (ctx instanceof short[]) return SHORT_ARRAY_HELPER; 62 | if (ctx instanceof int[]) return INT_ARRAY_HELPER; 63 | if (ctx instanceof long[]) return LONG_ARRAY_HELPER; 64 | if (ctx instanceof float[]) return FLOAT_ARRAY_HELPER; 65 | if (ctx instanceof double[]) return DOUBLE_ARRAY_HELPER; 66 | return null; 67 | } 68 | 69 | protected static final Mustache.VariableFetcher CUSTOM_FETCHER = new Mustache.VariableFetcher() { 70 | public Object get (Object ctx, String name) throws Exception { 71 | Mustache.CustomContext custom = (Mustache.CustomContext)ctx; 72 | Object val = custom.get(name); 73 | return val == null ? Template.NO_FETCHER_FOUND : val; 74 | } 75 | @Override public String toString () { 76 | return "CUSTOM_FETCHER"; 77 | } 78 | }; 79 | 80 | protected static final Mustache.VariableFetcher MAP_FETCHER = new Mustache.VariableFetcher() { 81 | public Object get (Object ctx, String name) throws Exception { 82 | Map map = (Map)ctx; 83 | if (map.containsKey(name)) return map.get(name); 84 | // special case to allow map entry set to be iterated over 85 | if ("entrySet".equals(name)) return map.entrySet(); 86 | return Template.NO_FETCHER_FOUND; 87 | } 88 | @Override public String toString () { 89 | return "MAP_FETCHER"; 90 | } 91 | }; 92 | 93 | protected static final Mustache.VariableFetcher LIST_FETCHER = new Mustache.VariableFetcher() { 94 | public Object get (Object ctx, String name) throws Exception { 95 | try { 96 | return ((List)ctx).get(Integer.parseInt(name)); 97 | } catch (NumberFormatException nfe) { 98 | return Template.NO_FETCHER_FOUND; 99 | } catch (IndexOutOfBoundsException e) { 100 | return Template.NO_FETCHER_FOUND; 101 | } 102 | } 103 | @Override public String toString () { 104 | return "LIST_FETCHER"; 105 | } 106 | }; 107 | 108 | protected static final Mustache.VariableFetcher ITER_FETCHER = new Mustache.VariableFetcher() { 109 | public Object get (Object ctx, String name) throws Exception { 110 | try { 111 | Iterator iter = (Iterator)ctx; 112 | for (int ii = 0, ll = Integer.parseInt(name); ii < ll; ii++) iter.next(); 113 | return iter.next(); 114 | } catch (NumberFormatException nfe) { 115 | return Template.NO_FETCHER_FOUND; 116 | } catch (NoSuchElementException e) { 117 | return Template.NO_FETCHER_FOUND; 118 | } 119 | } 120 | @Override public String toString () { 121 | return "ITER_FETCHER"; 122 | } 123 | }; 124 | 125 | protected static abstract class ArrayHelper implements Mustache.VariableFetcher { 126 | public Object get (Object ctx, String name) throws Exception { 127 | try { 128 | return get(ctx, Integer.parseInt(name)); 129 | } catch (NumberFormatException nfe) { 130 | return Template.NO_FETCHER_FOUND; 131 | } catch (ArrayIndexOutOfBoundsException e) { 132 | return Template.NO_FETCHER_FOUND; 133 | } 134 | } 135 | public abstract int length (Object ctx); 136 | protected abstract Object get (Object ctx, int index); 137 | } 138 | 139 | protected static final ArrayHelper OBJECT_ARRAY_HELPER = new ArrayHelper() { 140 | @Override protected Object get (Object ctx, int index) { return ((Object[])ctx)[index]; } 141 | @Override public int length (Object ctx) { return ((Object[])ctx).length; } 142 | }; 143 | protected static final ArrayHelper BOOLEAN_ARRAY_HELPER = new ArrayHelper() { 144 | @Override protected Object get (Object ctx, int index) { return ((boolean[])ctx)[index]; } 145 | @Override public int length (Object ctx) { return ((boolean[])ctx).length; } 146 | }; 147 | protected static final ArrayHelper BYTE_ARRAY_HELPER = new ArrayHelper() { 148 | @Override protected Object get (Object ctx, int index) { return ((byte[])ctx)[index]; } 149 | @Override public int length (Object ctx) { return ((byte[])ctx).length; } 150 | }; 151 | protected static final ArrayHelper CHAR_ARRAY_HELPER = new ArrayHelper() { 152 | @Override protected Object get (Object ctx, int index) { return ((char[])ctx)[index]; } 153 | @Override public int length (Object ctx) { return ((char[])ctx).length; } 154 | }; 155 | protected static final ArrayHelper SHORT_ARRAY_HELPER = new ArrayHelper() { 156 | @Override protected Object get (Object ctx, int index) { return ((short[])ctx)[index]; } 157 | @Override public int length (Object ctx) { return ((short[])ctx).length; } 158 | }; 159 | protected static final ArrayHelper INT_ARRAY_HELPER = new ArrayHelper() { 160 | @Override protected Object get (Object ctx, int index) { return ((int[])ctx)[index]; } 161 | @Override public int length (Object ctx) { return ((int[])ctx).length; } 162 | }; 163 | protected static final ArrayHelper LONG_ARRAY_HELPER = new ArrayHelper() { 164 | @Override protected Object get (Object ctx, int index) { return ((long[])ctx)[index]; } 165 | @Override public int length (Object ctx) { return ((long[])ctx).length; } 166 | }; 167 | protected static final ArrayHelper FLOAT_ARRAY_HELPER = new ArrayHelper() { 168 | @Override protected Object get (Object ctx, int index) { return ((float[])ctx)[index]; } 169 | @Override public int length (Object ctx) { return ((float[])ctx).length; } 170 | }; 171 | protected static final ArrayHelper DOUBLE_ARRAY_HELPER = new ArrayHelper() { 172 | @Override protected Object get (Object ctx, int index) { return ((double[])ctx)[index]; } 173 | @Override public int length (Object ctx) { return ((double[])ctx).length; } 174 | }; 175 | } 176 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.sonatype.oss 6 | oss-parent 7 | 9 8 | 9 | 10 | com.samskivert 11 | jmustache 12 | bundle 13 | 1.17-SNAPSHOT 14 | 15 | jmustache 16 | A Java implementation of the Mustache templating language. 17 | http://github.com/samskivert/jmustache 18 | 19 | http://github.com/samskivert/jmustache/issues 20 | 21 | 22 | 23 | 24 | BSD-2-Clause 25 | https://opensource.org/license/BSD-2-Clause 26 | repo 27 | 28 | 29 | 30 | 31 | 32 | samskivert 33 | Michael Bayne 34 | mdb@samskivert.com 35 | 36 | 37 | 38 | 39 | scm:git:git://github.com/samskivert/jmustache.git 40 | scm:git:git@github.com:samskivert/jmustache.git 41 | http://github.com/samskivert/jmustache 42 | 43 | 44 | 45 | 3.9.11 46 | 47 | 48 | 49 | 11 50 | UTF-8 51 | 52 | 2023-01-01T00:00:00Z 53 | 54 | 55 | 56 | 57 | junit 58 | junit 59 | 4.13.2 60 | test 61 | 62 | 63 | org.yaml 64 | snakeyaml 65 | 2.4 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-compiler-plugin 75 | 3.14.0 76 | 77 | ${source.level} 78 | ${source.level} 79 | ${source.level} 80 | true 81 | true 82 | true 83 | 84 | -Xlint 85 | -Xlint:-serial 86 | -Xlint:-path 87 | 88 | 89 | **/super/** 90 | 91 | 92 | 93 | 94 | default-compile 95 | 96 | 97 | module-info.java 98 | 99 | 11 100 | 101 | 102 | 103 | base-compile 104 | 105 | compile 106 | 107 | 108 | 109 | module-info.java 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | org.apache.felix 118 | maven-bundle-plugin 119 | 6.0.0 120 | true 121 | 122 | 123 | lazy 124 | <_removeheaders> 125 | Bnd*,Created-By,Include-Resource,Private-Package,Tool 126 | 127 | 128 | 129 | 130 | 131 | 132 | org.apache.maven.plugins 133 | maven-enforcer-plugin 134 | 3.6.1 135 | 136 | 137 | enforce-maven 138 | 139 | enforce 140 | 141 | 142 | 143 | 144 | 3.9.11 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | org.apache.maven.plugins 154 | maven-resources-plugin 155 | 3.3.1 156 | 157 | UTF-8 158 | 159 | 160 | 161 | 162 | org.apache.maven.plugins 163 | maven-surefire-plugin 164 | 3.5.3 165 | 166 | **/*Test.java 167 | 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-javadoc-plugin 173 | 3.11.2 174 | 175 | true 176 | public 177 | 178 | --module-path 179 | ${project.build.outputDirectory} 180 | -Xdoclint:all 181 | -Xdoclint:-missing 182 | -html5 183 | 184 | 185 | 186 | 187 | 188 | 189 | org.codehaus.mojo 190 | cobertura-maven-plugin 191 | 2.7 192 | 193 | xml 194 | 256m 195 | 196 | 197 | 198 | 199 | 200 | org.eluder.coveralls 201 | coveralls-maven-plugin 202 | 4.3.0 203 | 204 | 205 | 206 | 207 | org.sonatype.plugins 208 | nexus-staging-maven-plugin 209 | 1.7.0 210 | true 211 | false 212 | 213 | ossrh-releases 214 | https://oss.sonatype.org/ 215 | aaa740f9c5c260 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | release-sign-artifacts 224 | 225 | performReleasetrue 226 | 227 | 228 | 229 | 230 | org.apache.maven.plugins 231 | maven-gpg-plugin 232 | 3.2.8 233 | 234 | 235 | sign-artifacts 236 | verify 237 | 238 | sign 239 | 240 | 241 | 242 | 243 | mdb@samskivert.com 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | -------------------------------------------------------------------------------- /src/test/java/com/samskivert/mustache/MustacheTest.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache; 6 | 7 | import java.io.IOException; 8 | import java.io.Writer; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.Date; 13 | import java.util.HashMap; 14 | import java.util.Iterator; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.TimeZone; 18 | 19 | import org.junit.Test; 20 | import static org.junit.Assert.fail; 21 | 22 | /** 23 | * Mustache tests that can only be run on the JVM. Most tests should go in BaseMustacheTest so 24 | * that they can be run on GWT and the JVM to ensure they work in both places. 25 | */ 26 | public class MustacheTest extends SharedTests 27 | { 28 | @Test public void testFieldVariable () { 29 | test("bar", "{{foo}}", new Object() { 30 | String foo = "bar"; 31 | }); 32 | } 33 | 34 | @Test public void testMethodVariable () { 35 | test("bar", "{{foo}}", new Object() { 36 | String foo () { return "bar"; } 37 | }); 38 | } 39 | 40 | @Test public void testPropertyVariable () { 41 | test("bar", "{{foo}}", new Object() { 42 | String getFoo () { return "bar"; } 43 | }); 44 | } 45 | 46 | @Test public void testCustomContext() { 47 | test("bar", "{{foo}}", new Mustache.CustomContext() { 48 | @Override 49 | public Object get(String name) { 50 | return "foo".equals(name) ? "bar" : null; 51 | } 52 | }); 53 | } 54 | 55 | @Test public void testCharSequenceVariable() { 56 | Map ctx = new HashMap<>(); 57 | StringBuffer stringBuffer = new StringBuffer(); 58 | stringBuffer.append("bar"); 59 | ctx.put("foo", stringBuffer); 60 | test("bar", "{{foo}}", ctx); 61 | } 62 | 63 | public interface HasDefault { 64 | default String getFoo () { return "bar"; } 65 | } 66 | public interface Interloper extends HasDefault { 67 | default String getFoo () { return "bang"; } 68 | } 69 | @Test public void testDefaultMethodVariable () { 70 | test("bar", "{{foo}}", new HasDefault() { 71 | }); 72 | test("bang", "{{foo}}", new Interloper() { 73 | }); 74 | test("bong", "{{foo}}", new Interloper() { 75 | public String getFoo () { return "bong"; } 76 | }); 77 | } 78 | 79 | @Test public void testBooleanPropertyVariable () { 80 | test("true", "{{foo}}", new Object() { 81 | Boolean isFoo () { return true; } 82 | }); 83 | } 84 | 85 | @Test public void testPrimitiveBooleanPropertyVariable () { 86 | test("false", "{{foo}}", new Object() { 87 | boolean isFoo () { return false; } 88 | }); 89 | } 90 | 91 | @Test public void testSkipVoidReturn () { 92 | test("bar", "{{foo}}", new Object() { 93 | void foo () {} 94 | String getFoo () { return "bar"; } 95 | }); 96 | } 97 | 98 | @Test public void testCallSiteReuse () { 99 | Template tmpl = Mustache.compiler().compile("{{foo}}"); 100 | Object ctx = new Object() { 101 | String getFoo () { return "bar"; } 102 | }; 103 | for (int ii = 0; ii < 50; ii++) { 104 | check("bar", tmpl.execute(ctx)); 105 | } 106 | } 107 | 108 | @Test public void testCallSiteChange () { 109 | Template tmpl = Mustache.compiler().compile("{{foo}}"); 110 | check("bar", tmpl.execute(new Object() { 111 | String getFoo () { return "bar"; } 112 | })); 113 | check("bar", tmpl.execute(new Object() { 114 | String foo = "bar"; 115 | })); 116 | } 117 | 118 | @Test public void testSectionWithNonFalseyZero () { 119 | test(Mustache.compiler(), "test", "{{#foo}}test{{/foo}}", new Object() { 120 | Long foo = 0L; 121 | }); 122 | } 123 | 124 | @Test public void testSectionWithFalseyZero () { 125 | test(Mustache.compiler().zeroIsFalse(true), "", 126 | "{{#intv}}intv{{/intv}}" + 127 | "{{#longv}}longv{{/longv}}" + 128 | "{{#floatv}}floatv{{/floatv}}" + 129 | "{{#doublev}}doublev{{/doublev}}" + 130 | "{{#longm}}longm{{/longm}}" + 131 | "{{#intm}}intm{{/intm}}" + 132 | "{{#floatm}}floatm{{/floatm}}" + 133 | "{{#doublem}}doublem{{/doublem}}", 134 | new Object() { 135 | Integer intv = 0; 136 | Long longv = 0L; 137 | Float floatv = 0f; 138 | Double doublev = 0d; 139 | int intm () { return 0; } 140 | long longm () { return 0l; } 141 | float floatm () { return 0f; } 142 | double doublem () { return 0d; } 143 | }); 144 | } 145 | 146 | @Test public void testOptionalSupportingCollector () { 147 | Mustache.Compiler comp = Mustache.compiler().withCollector(new DefaultCollector() { 148 | public Iterator toIterator (final Object value) { 149 | if (value instanceof Optional) { 150 | Optional opt = (Optional) value; 151 | return opt.isPresent() ? Collections.singleton(opt.get()).iterator() : 152 | Collections.emptyList().iterator(); 153 | } else return super.toIterator(value); 154 | } 155 | }); 156 | test(comp, "test", "{{#foo}}{{.}}{{/foo}}", context("foo", Optional.of("test"))); 157 | test(comp, "", "{{#foo}}{{.}}{{/foo}}", context("foo", Optional.empty())); 158 | test(comp, "", "{{^foo}}{{.}}{{/foo}}", context("foo", Optional.of("test"))); 159 | test(comp, "test", "{{^foo}}test{{/foo}}", context("foo", Optional.empty())); 160 | } 161 | 162 | @Test public void testCompoundVariable () { 163 | test("hello", "{{foo.bar.baz}}", new Object() { 164 | Object foo () { 165 | return new Object() { 166 | Object bar = new Object() { 167 | String baz = "hello"; 168 | }; 169 | }; 170 | } 171 | }); 172 | } 173 | 174 | @Test public void testNullComponentInCompoundVariable () { 175 | try { 176 | test(Mustache.compiler(), "unused", "{{foo.bar.baz}}", new Object() { 177 | Object foo = new Object() { 178 | Object bar = null; 179 | }; 180 | }); 181 | fail(); 182 | } catch (MustacheException me) {} // expected 183 | } 184 | 185 | @Test public void testMissingComponentInCompoundVariable () { 186 | try { 187 | test(Mustache.compiler(), "unused", "{{foo.bar.baz}}", new Object() { 188 | Object foo = new Object(); // no bar 189 | }); 190 | fail(); 191 | } catch (MustacheException me) {} // expected 192 | } 193 | 194 | @Test public void testNullComponentInCompoundVariableWithDefault () { 195 | test(Mustache.compiler().nullValue("null"), "null", "{{foo.bar.baz}}", new Object() { 196 | Object foo = null; 197 | }); 198 | test(Mustache.compiler().nullValue("null"), "null", "{{foo.bar.baz}}", new Object() { 199 | Object foo = new Object() { 200 | Object bar = null; 201 | }; 202 | }); 203 | } 204 | 205 | @Test public void testMissingComponentInCompoundVariableWithDefault () { 206 | test(Mustache.compiler().defaultValue("?"), "?", "{{foo.bar.baz}}", new Object() { 207 | // no foo, no bar 208 | }); 209 | test(Mustache.compiler().defaultValue("?"), "?", "{{foo.bar.baz}}", new Object() { 210 | Object foo = new Object(); // no bar 211 | }); 212 | } 213 | 214 | @Test public void testCompoundVariableAsPlain () { 215 | // if a compound variable is found without decomposition, we use that first 216 | test("wholekey", "{{foo.bar}}", context( 217 | "foo.bar", "wholekey", 218 | "foo", new Object() { String bar = "hello"; })); 219 | } 220 | 221 | @Test public void testShadowedContextWithNull () { 222 | Mustache.Compiler comp = Mustache.compiler().nullValue("(null)"); 223 | String tmpl = "{{foo}}{{#inner}}{{foo}}{{/inner}}", expect = "outer(null)"; 224 | test(comp, expect, tmpl, new Object() { 225 | public String foo = "outer"; 226 | public Object inner = new Object() { 227 | // this foo should shadow the outer foo even though it's null 228 | public String foo = null; 229 | }; 230 | }); 231 | // same as above, but with maps instead of objects 232 | test(comp, expect, tmpl, context("foo", "outer", "inner", context("foo", null))); 233 | } 234 | 235 | @Test public void testContextPokingLambda () { 236 | Mustache.Compiler c = Mustache.compiler(); 237 | 238 | class Foo { public int foo = 1; } 239 | final Template lfoo = c.compile("{{foo}}"); 240 | check("1", lfoo.execute(new Foo())); 241 | 242 | class Bar { public String bar = "one"; } 243 | final Template lbar = c.compile("{{bar}}"); 244 | check("one", lbar.execute(new Bar())); 245 | 246 | test(c, "1oneone1one!", "{{#events}}{{#renderEvent}}{{/renderEvent}}{{/events}}", context( 247 | "renderEvent", new Mustache.Lambda() { 248 | public void execute (Template.Fragment frag, Writer out) throws IOException { 249 | Object ctx = frag.context(); 250 | if (ctx instanceof Foo) lfoo.execute(ctx, out); 251 | else if (ctx instanceof Bar) lbar.execute(ctx, out); 252 | else out.write("!"); 253 | } 254 | }, 255 | "events", Arrays.asList( 256 | new Foo(), new Bar(), new Bar(), new Foo(), new Bar(), "wtf?"))); 257 | } 258 | 259 | @Test public void testCustomFormatter () { 260 | Mustache.Formatter fmt = new Mustache.Formatter() { 261 | public String format (Object value) { 262 | if (value instanceof Date) return _fmt.format((Date)value); 263 | else return String.valueOf(value); 264 | } 265 | protected SimpleDateFormat _fmt = new SimpleDateFormat("yyyy/MM/dd"); { 266 | _fmt.setTimeZone(TimeZone.getTimeZone("UTC")); 267 | } 268 | }; 269 | check("Date: 2014/01/08", Mustache.compiler().withFormatter(fmt). 270 | compile("{{msg}}: {{today}}").execute(new Object() { 271 | String msg = "Date"; 272 | Date today = new Date(1389208567874L); 273 | })); 274 | } 275 | 276 | @Test public void testMapEntriesPlusReflectSection () { 277 | Map data = new HashMap(); 278 | data.put("k1", "v1"); 279 | data.put("k2", "v2"); 280 | // 'key' and 'value' here rely on reflection so we can't test this in GWT 281 | test("k1v1k2v2", "{{#map.entrySet}}{{key}}{{value}}{{/map.entrySet}}", 282 | context("map", data)); 283 | } 284 | 285 | @Test public void testNoAccesCoercion () { 286 | Object ctx = new Object() { 287 | public Object foo () { 288 | return new Object() { 289 | public Object bar = new Object() { 290 | public String baz = "hello"; 291 | private String quux = "hello"; 292 | }; 293 | }; 294 | } 295 | }; 296 | test("hello:hello", "{{foo.bar.baz}}:{{foo.bar.quux}}", ctx); 297 | Mustache.Compiler comp = Mustache.compiler(). 298 | withCollector(new DefaultCollector(false)). 299 | defaultValue("missing"); 300 | test(comp, "hello:missing", "{{foo.bar.baz}}:{{foo.bar.quux}}", ctx); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.2.0 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=$(java-config --jre-home) 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && 89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="$(which javac)" 94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=$(which readlink) 97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 98 | if $darwin ; then 99 | javaHome="$(dirname "\"$javaExecutable\"")" 100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" 101 | else 102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")" 103 | fi 104 | javaHome="$(dirname "\"$javaExecutable\"")" 105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=$(cd "$wdir/.." || exit 1; pwd) 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | # Remove \r in case we run on Windows within Git Bash 164 | # and check out the repository with auto CRLF management 165 | # enabled. Otherwise, we may read lines that are delimited with 166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 167 | # splitting rules. 168 | tr -s '\r\n' ' ' < "$1" 169 | fi 170 | } 171 | 172 | log() { 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | printf '%s\n' "$1" 175 | fi 176 | } 177 | 178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 179 | if [ -z "$BASE_DIR" ]; then 180 | exit 1; 181 | fi 182 | 183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 184 | log "$MAVEN_PROJECTBASEDIR" 185 | 186 | ########################################################################################## 187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 188 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 189 | ########################################################################################## 190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 191 | if [ -r "$wrapperJarPath" ]; then 192 | log "Found $wrapperJarPath" 193 | else 194 | log "Couldn't find $wrapperJarPath, downloading it ..." 195 | 196 | if [ -n "$MVNW_REPOURL" ]; then 197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 198 | else 199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 200 | fi 201 | while IFS="=" read -r key value; do 202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) 203 | safeValue=$(echo "$value" | tr -d '\r') 204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; 205 | esac 206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 207 | log "Downloading from: $wrapperUrl" 208 | 209 | if $cygwin; then 210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 211 | fi 212 | 213 | if command -v wget > /dev/null; then 214 | log "Found wget ... using wget" 215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 218 | else 219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 220 | fi 221 | elif command -v curl > /dev/null; then 222 | log "Found curl ... using curl" 223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 228 | fi 229 | else 230 | log "Falling back to using Java to download" 231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 233 | # For Cygwin, switch paths to Windows format before running javac 234 | if $cygwin; then 235 | javaSource=$(cygpath --path --windows "$javaSource") 236 | javaClass=$(cygpath --path --windows "$javaClass") 237 | fi 238 | if [ -e "$javaSource" ]; then 239 | if [ ! -e "$javaClass" ]; then 240 | log " - Compiling MavenWrapperDownloader.java ..." 241 | ("$JAVA_HOME/bin/javac" "$javaSource") 242 | fi 243 | if [ -e "$javaClass" ]; then 244 | log " - Running MavenWrapperDownloader.java ..." 245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 246 | fi 247 | fi 248 | fi 249 | fi 250 | ########################################################################################## 251 | # End of extension 252 | ########################################################################################## 253 | 254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 255 | wrapperSha256Sum="" 256 | while IFS="=" read -r key value; do 257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; 258 | esac 259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 260 | if [ -n "$wrapperSha256Sum" ]; then 261 | wrapperSha256Result=false 262 | if command -v sha256sum > /dev/null; then 263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then 264 | wrapperSha256Result=true 265 | fi 266 | elif command -v shasum > /dev/null; then 267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then 268 | wrapperSha256Result=true 269 | fi 270 | else 271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." 272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." 273 | exit 1 274 | fi 275 | if [ $wrapperSha256Result = false ]; then 276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 279 | exit 1 280 | fi 281 | fi 282 | 283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 284 | 285 | # For Cygwin, switch paths to Windows format before running java 286 | if $cygwin; then 287 | [ -n "$JAVA_HOME" ] && 288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 289 | [ -n "$CLASSPATH" ] && 290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 291 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 293 | fi 294 | 295 | # Provide a "standardized" way to retrieve the CLI args that will 296 | # work with both Windows and non-Windows executions. 297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 298 | export MAVEN_CMD_LINE_ARGS 299 | 300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 301 | 302 | # shellcheck disable=SC2086 # safe args 303 | exec "$JAVACMD" \ 304 | $MAVEN_OPTS \ 305 | $MAVEN_DEBUG_OPTS \ 306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 309 | -------------------------------------------------------------------------------- /src/main/java/com/samskivert/mustache/Template.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache; 6 | 7 | import java.io.IOException; 8 | import java.io.StringWriter; 9 | import java.io.Writer; 10 | import java.util.Collections; 11 | import java.util.Iterator; 12 | import java.util.Map; 13 | 14 | import com.samskivert.mustache.Mustache.BlockSegment; 15 | 16 | /** 17 | * Represents a compiled template. Templates are executed with a context to generate 18 | * output. The context can be any tree of objects. Variables are resolved against the context. 19 | * Given a name {@code foo}, the following mechanisms are supported for resolving its value 20 | * (and are sought in this order): 21 | * 22 | *
    23 | *
  • If the variable has the special name {@code this} the context object itself will be 24 | * returned. This is useful when iterating over lists. 25 | *
  • If the object is a {@link Map}, {@link Map#get} will be called with the string {@code foo} 26 | * as the key. 27 | *
  • A method named {@code foo} in the supplied object (with non-void return value). 28 | *
  • A method named {@code getFoo} in the supplied object (with non-void return value). 29 | *
  • A field named {@code foo} in the supplied object. 30 | *
31 | * 32 | *

The field type, method return type, or map value type should correspond to the desired 33 | * behavior if the resolved name corresponds to a section. {@link Boolean} is used for showing or 34 | * hiding sections without binding a sub-context. Arrays, {@link Iterator} and {@link Iterable} 35 | * implementations are used for sections that repeat, with the context bound to the elements of the 36 | * array, iterator or iterable. Lambdas are current unsupported, though they would be easy enough 37 | * to add if desire exists. See the Mustache 38 | * documentation for more details on section behavior.

39 | */ 40 | public class Template { 41 | 42 | /** 43 | * Encapsulates a fragment of a template that is passed to a lambda. The fragment is bound to 44 | * the variable context that was in effect at the time the lambda was called. 45 | */ 46 | public abstract class Fragment { 47 | 48 | /** Executes this fragment; writes its result to {@code out}. */ 49 | public abstract void execute (Writer out); 50 | 51 | /** Executes this fragment with the provided context; writes its result to {@code out}. The 52 | * provided context will be nested in the fragment's bound context. */ 53 | public abstract void execute (Object context, Writer out); 54 | 55 | /** Executes {@code tmpl} using this fragment's bound context. This allows a lambda to 56 | * resolve its fragment to a dynamically loaded template and then run that template with 57 | * the same context as the lamda, allowing a lambda to act as a 'late bound' included 58 | * template, i.e. you can decide which template to include based on information in the 59 | * context. */ 60 | public abstract void executeTemplate (Template tmpl, Writer out); 61 | 62 | /** Executes this fragment and returns its result as a string. */ 63 | public String execute () { 64 | StringWriter out = new StringWriter(); 65 | execute(out); 66 | return out.toString(); 67 | } 68 | 69 | /** Executes this fragment with the provided context; returns its result as a string. The 70 | * provided context will be nested in the fragment's bound context. */ 71 | public String execute (Object context) { 72 | StringWriter out = new StringWriter(); 73 | execute(context, out); 74 | return out.toString(); 75 | } 76 | 77 | /** Returns the context object in effect for this fragment. The actual type of the object 78 | * depends on the structure of the data passed to the top-level template. You know where 79 | * your lambdas are executed, so you know what type to which to cast the context in order 80 | * to inspect it (be that a {@code Map} or a POJO or something else). */ 81 | public abstract Object context (); 82 | 83 | /** Like {@link #context()} btu returns the {@code n}th parent context object. {@code 0} 84 | * returns the same value as {@link #context()}, {@code 1} returns the parent context, 85 | * {@code 2} returns the grandparent and so forth. Note that if you request a parent that 86 | * does not exist an exception will be thrown. You should only use this method when you 87 | * know your lambda is run consistently in a context with a particular lineage. */ 88 | public abstract Object context (int n); 89 | 90 | /** Decompiles the template inside this lamdba and returns an approximation of 91 | * the original template from which it was parsed. This is not the exact character for 92 | * character representation because the original text is not preserved because that would 93 | * incur a huge memory penalty for all users of the library when the vast majority of 94 | * them do not call decompile. 95 | * 96 | *

Limitations: 97 | *

  • Whitespace inside tags is not preserved: i.e. {@code {{ foo.bar }}} becomes 98 | * {@code {{foo.bar}}}. 99 | *
  • If the delimiters are changed by the template, those are not preserved. 100 | * The delimiters configured on the {@link Compiler} are used for all decompilation. 101 | *
102 | * 103 | *

This feature is meant to enable use of lambdas for i18n such that you can recover 104 | * the contents of a lambda (so long as they're simple) to use as the lookup key for a 105 | * translation string. For example: {@code {{#i18n}}Hello {{user.name}}!{{/i18n}}} can be 106 | * sent to an {@code i18n} lambda which can use {@code decompile} to recover the text 107 | * {@code Hello {{user.name}}!} to be looked up in a translation dictionary. The 108 | * translated fragment could then be compiled and cached and then executed in lieu of the 109 | * original fragment using {@link Template.Fragment#context}. 110 | */ 111 | public String decompile () { 112 | return decompile(new StringBuilder()).toString(); 113 | } 114 | 115 | /** Decompiles this fragment into {@code into}. See {@link #decompile()}. 116 | * @return {@code into} for call chaining. */ 117 | public abstract StringBuilder decompile (StringBuilder into); 118 | } 119 | 120 | /** A sentinel object that can be returned by a {@link Mustache.Collector} to indicate that a 121 | * variable does not exist in a particular context. */ 122 | public static final Object NO_FETCHER_FOUND = new String(""); 123 | 124 | /** 125 | * Executes this template with the given context, returning the results as a string. 126 | * @throws MustacheException if an error occurs while executing or writing the template. 127 | */ 128 | public String execute (Object context) throws MustacheException { 129 | StringWriter out = new StringWriter(); 130 | execute(context, out); 131 | return out.toString(); 132 | } 133 | 134 | /** 135 | * Executes this template with the given context, writing the results to the supplied writer. 136 | * @throws MustacheException if an error occurs while executing or writing the template. 137 | */ 138 | public void execute (Object context, Writer out) throws MustacheException { 139 | executeSegs(new Context(context, null, 0, false, false), out); 140 | } 141 | 142 | /** 143 | * Executes this template with the supplied context and parent context, writing the results to 144 | * the supplied writer. The parent context will be searched for variables that cannot be found 145 | * in the main context, in the same way the main context becomes a parent context when entering 146 | * a block. 147 | * @throws MustacheException if an error occurs while executing or writing the template. 148 | */ 149 | public void execute (Object context, Object parentContext, Writer out) throws MustacheException { 150 | Context pctx = new Context(parentContext, null, 0, false, false); 151 | executeSegs(new Context(context, pctx, 0, false, false), out); 152 | } 153 | 154 | /** 155 | * Visits the tags in this template (via {@code visitor}) without executing it. 156 | * @param visitor the visitor to be called back on each tag in the template. 157 | */ 158 | public void visit (Mustache.Visitor visitor) { 159 | for (Segment seg : _segs) { 160 | seg.visit(visitor); 161 | } 162 | } 163 | 164 | protected Template (Segment[] segs, Mustache.Compiler compiler) { 165 | _segs = segs; 166 | _compiler = compiler; 167 | _fcache = compiler.collector.createFetcherCache(); 168 | } 169 | 170 | protected Template indent (String indent) { 171 | // What we want to do here is rebuild this partial template but indented. 172 | // If identing does not change anything we return the original template. 173 | if (indent.equals("")) { 174 | return this; 175 | } 176 | Segment[] copySegs = Mustache.indentSegs(_segs, indent, false,false); 177 | if (copySegs == _segs) { 178 | return this; 179 | } 180 | return new Template(copySegs, _compiler); 181 | } 182 | 183 | protected Template replaceBlocks (Map blocks) { 184 | if (blocks.isEmpty()) { 185 | return this; 186 | } 187 | Segment[] copySegs = Mustache.replaceBlockSegs(_segs, blocks); 188 | if (copySegs == _segs) { 189 | return this; 190 | } 191 | return new Template(copySegs, _compiler); 192 | } 193 | 194 | protected void executeSegs (Context ctx, Writer out) throws MustacheException { 195 | for (Segment seg : _segs) { 196 | seg.execute(this, ctx, out); 197 | } 198 | } 199 | 200 | protected Fragment createFragment (final Segment[] segs, final Context currentCtx) { 201 | return new Fragment() { 202 | @Override public void execute (Writer out) { 203 | execute(currentCtx, out); 204 | } 205 | @Override public void execute (Object context, Writer out) { 206 | execute(currentCtx.nest(context), out); 207 | } 208 | @Override public void executeTemplate (Template tmpl, Writer out) { 209 | tmpl.executeSegs(currentCtx, out); 210 | } 211 | @Override public Object context () { 212 | return currentCtx.data; 213 | } 214 | @Override public Object context (int n) { 215 | return context(currentCtx, n); 216 | } 217 | @Override public StringBuilder decompile (StringBuilder into) { 218 | for (Segment seg : segs) seg.decompile(_compiler.delims, into); 219 | return into; 220 | } 221 | private Object context (Context ctx, int n) { 222 | return (n == 0) ? ctx.data : context(ctx.parent, n-1); 223 | } 224 | private void execute (Context ctx, Writer out) { 225 | for (Segment seg : segs) { 226 | seg.execute(Template.this, ctx, out); 227 | } 228 | } 229 | }; 230 | } 231 | 232 | /** 233 | * Called by executing segments to obtain the value of the specified variable in the supplied 234 | * context. 235 | * 236 | * @param ctx the context in which to look up the variable. 237 | * @param name the name of the variable to be resolved. 238 | * @param missingIsNull whether to fail if a variable cannot be resolved, or to return null in 239 | * that case. 240 | * 241 | * @return the value associated with the supplied name or null if no value could be resolved. 242 | */ 243 | protected Object getValue (Context ctx, String name, int line, boolean missingIsNull) { 244 | // handle our special variables 245 | if (name.equals(FIRST_NAME)) { 246 | return ctx.onFirst; 247 | } else if (name.equals(LAST_NAME)) { 248 | return ctx.onLast; 249 | } else if (name.equals(INDEX_NAME)) { 250 | return ctx.index; 251 | } 252 | 253 | // if we're in standards mode, restrict ourselves to simple direct resolution (no compound 254 | // keys, no resolving values in parent contexts) 255 | if (_compiler.standardsMode) { 256 | Object value = getValueIn(ctx.data, name, line); 257 | return checkForMissing(name, line, missingIsNull, value); 258 | } 259 | 260 | // first search our parent contexts for the key (even if the key is a compound key, we will 261 | // first try to find it "whole" and only if that fails do we resolve it in parts) 262 | for (Context pctx = ctx; pctx != null; pctx = pctx.parent) { 263 | Object value = getValueIn(pctx.data, name, line); 264 | if (value != NO_FETCHER_FOUND) return value; 265 | } 266 | // if we reach here, we found nothing in this or our parent contexts... 267 | 268 | // if we have a compound key, decompose the value and resolve it step by step 269 | if (!name.equals(DOT_NAME) && name.indexOf(DOT_NAME) != -1) { 270 | return getCompoundValue(ctx, name, line, missingIsNull); 271 | } else { 272 | // otherwise let checkForMissing() decide what to do 273 | return checkForMissing(name, line, missingIsNull, NO_FETCHER_FOUND); 274 | } 275 | } 276 | 277 | /** 278 | * Decomposes the compound key {@code name} into components and resolves the value they 279 | * reference. 280 | */ 281 | protected Object getCompoundValue (Context ctx, String name, int line, boolean missingIsNull) { 282 | String[] comps = name.split("\\."); 283 | // we want to allow the first component of a compound key to be located in a parent 284 | // context, but once we're selecting sub-components, they must only be resolved in the 285 | // object that represents that component 286 | Object data = getValue(ctx, comps[0], line, missingIsNull); 287 | for (int ii = 1; ii < comps.length; ii++) { 288 | if (data == NO_FETCHER_FOUND) { 289 | if (!missingIsNull) throw new MustacheException.Context( 290 | "Missing context for compound variable '" + name + "' on line " + line + 291 | ". '" + comps[ii - 1] + "' was not found.", name, line); 292 | return null; 293 | } else if (data == null) { 294 | return null; 295 | } 296 | // once we step into a composite key, we drop the ability to query our parent contexts; 297 | // that would be weird and confusing 298 | data = getValueIn(data, comps[ii], line); 299 | } 300 | return checkForMissing(name, line, missingIsNull, data); 301 | } 302 | 303 | /** 304 | * Returns the value of the specified variable, noting that it is intended to be used as the 305 | * contents for a section. 306 | */ 307 | protected Object getSectionValue (Context ctx, String name, int line) { 308 | Object value = getValue(ctx, name, line, !_compiler.strictSections); 309 | // TODO: configurable behavior on null values? 310 | return (value == null) ? Collections.emptyList() : value; 311 | } 312 | 313 | /** 314 | * Returns the value for the specified variable, or the configured default value if the 315 | * variable resolves to null. See {@link #getValue}. 316 | */ 317 | protected Object getValueOrDefault (Context ctx, String name, int line) { 318 | Object value = getValue(ctx, name, line, _compiler.missingIsNull); 319 | // getValue will raise MustacheException if a variable cannot be resolved and missingIsNull 320 | // is not configured; so we're safe to assume that any null that makes it up to this point 321 | // can be converted to nullValue 322 | return (value == null) ? _compiler.computeNullValue(name) : value; 323 | } 324 | 325 | protected Object getValueIn (Object data, String name, int line) { 326 | // if we're getting `.` or `this` then just return the whole context; we do this before the 327 | // null check because it may be valid for the context to be null (if we're iterating over a 328 | // list which contains nulls, for example) 329 | if (isThisName(name)) return data; 330 | 331 | if (data == null) { 332 | throw new NullPointerException( 333 | "Null context for variable '" + name + "' on line " + line); 334 | } 335 | 336 | Key key = new Key(data.getClass(), name); 337 | Mustache.VariableFetcher fetcher = _fcache.get(key); 338 | if (fetcher != null) { 339 | try { 340 | return fetcher.get(data, name); 341 | } catch (Exception e) { 342 | // zoiks! non-monomorphic call site, update the cache and try again 343 | fetcher = _compiler.collector.createFetcher(data, key.name); 344 | } 345 | } else { 346 | fetcher = _compiler.collector.createFetcher(data, key.name); 347 | } 348 | 349 | // if we were unable to create a fetcher, use the NOT_FOUND_FETCHER which will return 350 | // NO_FETCHER_FOUND to let the caller know that they can try the parent context or do le 351 | // freak out; we still cache this fetcher to avoid repeatedly looking up and failing to 352 | // find a fetcher in the same context (which can be expensive) 353 | if (fetcher == null) { 354 | fetcher = NOT_FOUND_FETCHER; 355 | } 356 | 357 | try { 358 | Object value = fetcher.get(data, name); 359 | _fcache.put(key, fetcher); 360 | return value; 361 | } catch (Exception e) { 362 | throw new MustacheException.Context( 363 | "Failure fetching variable '" + name + "' on line " + line, name, line, e); 364 | } 365 | } 366 | 367 | protected Object checkForMissing (String name, int line, boolean missingIsNull, Object value) { 368 | if (value == NO_FETCHER_FOUND) { 369 | if (missingIsNull) return null; 370 | throw new MustacheException.Context( 371 | "No method or field with name '" + name + "' on line " + line, name, line); 372 | } else { 373 | return value; 374 | } 375 | } 376 | 377 | protected final Segment[] _segs; 378 | protected final Mustache.Compiler _compiler; 379 | protected final Map _fcache; 380 | 381 | protected static class Context { 382 | public final Object data; 383 | public final Context parent; 384 | public final int index; 385 | public final boolean onFirst; 386 | public final boolean onLast; 387 | 388 | public Context (Object data, Context parent, int index, boolean onFirst, boolean onLast) { 389 | this.data = data; 390 | this.parent = parent; 391 | this.index = index; 392 | this.onFirst = onFirst; 393 | this.onLast = onLast; 394 | } 395 | 396 | public Context nest (Object data) { 397 | return new Context(data, this, index, onFirst, onLast); 398 | } 399 | 400 | public Context nest (Object data, int index, boolean onFirst, boolean onLast) { 401 | return new Context(data, this, index, onFirst, onLast); 402 | } 403 | } 404 | 405 | /** A template is broken into segments. */ 406 | protected static abstract class Segment { 407 | abstract void execute (Template tmpl, Context ctx, Writer out); 408 | abstract void decompile (Mustache.Delims delims, StringBuilder into); 409 | abstract void visit (Mustache.Visitor visitor); 410 | 411 | /** 412 | * Recursively indent by the parameter indent. 413 | * @param indent should be space characters that are not {@code \n}. 414 | * @param first append indent to the first line (regardless if it has a {@code \n} or not). 415 | * @param last append indent on the last {@code \n} that has no text after it. 416 | * @return a newly created segment or the same segment if nothing changed. 417 | */ 418 | abstract Segment indent (String indent, boolean first, boolean last); 419 | 420 | /** 421 | * Whether or not the segment is standalone. The definition of standalone is defined by the 422 | * mustache spec. String and variable tags are never standalone. For blocks this is based on 423 | * the closing tag. Once {@code trim} is called, standalone tags are determined so that 424 | * proper (re)indentation will work without reparsing the template. 425 | * @return true if the tag is standalone. 426 | */ 427 | abstract boolean isStandalone (); 428 | 429 | protected static void write (Writer out, CharSequence data) { 430 | try { 431 | out.append(data); 432 | } catch (IOException ioe) { 433 | throw new MustacheException(ioe); 434 | } 435 | } 436 | 437 | protected static void escape (Appendable out, CharSequence data, Mustache.Escaper escape) { 438 | try { 439 | escape.escape(out, data); 440 | } catch (IOException ioe) { 441 | throw new MustacheException(ioe); 442 | } 443 | } 444 | } 445 | 446 | /** Used to cache variable fetchers for a given context class, name combination. */ 447 | protected static class Key { 448 | public final Class cclass; 449 | public final String name; 450 | 451 | public Key (Class cclass, String name) { 452 | this.cclass = cclass; 453 | this.name = name; 454 | } 455 | 456 | @Override public int hashCode () { 457 | return cclass.hashCode() * 31 + name.hashCode(); 458 | } 459 | 460 | @Override public boolean equals (Object other) { 461 | Key okey = (Key)other; 462 | return okey.cclass == cclass && okey.name.equals(name); 463 | } 464 | 465 | @Override public String toString () { 466 | return cclass.getName() + ":" + name; 467 | } 468 | } 469 | 470 | protected static boolean isThisName (String name) { 471 | return DOT_NAME.equals(name) || THIS_NAME.equals(name); 472 | } 473 | 474 | protected static final String DOT_NAME = "."; 475 | protected static final String THIS_NAME = "this"; 476 | protected static final String FIRST_NAME = "-first"; 477 | protected static final String LAST_NAME = "-last"; 478 | protected static final String INDEX_NAME = "-index"; 479 | 480 | /** A fetcher cached for lookups that failed to find a fetcher. */ 481 | protected static Mustache.VariableFetcher NOT_FOUND_FETCHER = new Mustache.VariableFetcher() { 482 | public Object get (Object ctx, String name) throws Exception { 483 | return NO_FETCHER_FOUND; 484 | } 485 | }; 486 | } 487 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a Java implementation of the [Mustache template language](http://mustache.github.io/). 2 | 3 | [![Build Status](https://travis-ci.org/samskivert/jmustache.svg?branch=master)](https://travis-ci.org/samskivert/jmustache) 4 | 5 | Motivations 6 | =========== 7 | 8 | * Zero dependencies. You can include this single tiny library in your project and start making 9 | use of templates. 10 | * Usability on a variety of target platforms. This implementation makes very limited demands on 11 | the JVM in which it runs and as a result is usable on Android, or on other limited JVMs. It is 12 | even possible to avoid the use of reflection and provide all of your data as a series of nested 13 | maps. 14 | 15 | * [Proguard](http://proguard.sourceforge.net/) and [JarJar](http://code.google.com/p/jarjar/) 16 | friendly. Though the library will reflectively access your data (if you desire it), the library 17 | makes no other internal use of reflection or by name instantiation of classes. So you can embed 18 | it using Proguard or JarJar without any annoying surprises. 19 | 20 | * Minimal API footprint. There are really only two methods you need to know about: `compile` and 21 | `execute`. You can even chain them together in cases where performance is of no consequence. 22 | 23 | Its existence justified by the above motivations, this implementation then strives to provide 24 | additional benefits: 25 | 26 | * It is available via Maven Central, see below for details. 27 | * It is reasonably performant. Templates are parsed separately from execution. A template will 28 | specialize its variables on (class of context, name) pairs so that if a variable is first 29 | resolved to be (for example) a field of the context object, that will be attempted directly on 30 | subsequent template invocations, and the slower full resolution will only be tried if accessing 31 | the variable as a field fails. 32 | 33 | Get It 34 | ====== 35 | 36 | JMustache is available via Maven Central and can thus be easily added to your Maven, Ivy, etc. 37 | projects by adding a dependency on `com.samskivert:jmustache:1.15`. Or download the pre-built 38 | [jar file](https://repo1.maven.org/maven2/com/samskivert/jmustache/1.15/jmustache-1.15.jar). 39 | 40 | Documentation 41 | ============= 42 | 43 | In addition to the usage section below, the following documentation may be useful: 44 | 45 | * [API docs](http://samskivert.github.io/jmustache/apidocs/) 46 | * [Mustache manual](http://mustache.github.io/mustache.5.html) 47 | 48 | Usage 49 | ===== 50 | 51 | Using JMustache is very simple. Supply your template as a `String` or a `Reader` and get back a 52 | `Template` that you can execute on any context: 53 | 54 | ```java 55 | String text = "One, two, {{three}}. Three sir!"; 56 | Template tmpl = Mustache.compiler().compile(text); 57 | Map data = new HashMap(); 58 | data.put("three", "five"); 59 | System.out.println(tmpl.execute(data)); 60 | // result: "One, two, five. Three sir!" 61 | ``` 62 | 63 | Use `Reader` and `Writer` if you're doing something more serious: 64 | 65 | ```java 66 | void executeTemplate (Reader template, Writer out, Map data) { 67 | Mustache.compiler().compile(template).execute(data, out); 68 | } 69 | ``` 70 | 71 | The execution context can be any Java object. Variables will be resolved via the following 72 | mechanisms: 73 | 74 | * If the context is a `MustacheCustomContext`, `MustacheCustomContext.get` will be used. 75 | * If the context is a `Map`, `Map.get` will be used. 76 | * If a non-void method with the same name as the variable exists, it will be called. 77 | * If a non-void method named (for variable `foo`) `getFoo` exists, it will be called. 78 | * If a field with the same name as the variable exists, its contents will be used. 79 | 80 | Example: 81 | 82 | ```java 83 | class Person { 84 | public final String name; 85 | public Person (String name, int age) { 86 | this.name = name; 87 | _age = age; 88 | } 89 | public int getAge () { 90 | return _age; 91 | } 92 | protected int _age; 93 | } 94 | 95 | String tmpl = "{{#persons}}{{name}}: {{age}}\n{{/persons}}"; 96 | Mustache.compiler().compile(tmpl).execute(new Object() { 97 | Object persons = Arrays.asList(new Person("Elvis", 75), new Person("Madonna", 52)); 98 | }); 99 | 100 | // result: 101 | // Elvis: 75 102 | // Madonna: 52 103 | ``` 104 | 105 | As you can see from the example, the fields (and methods) need not be public. The `persons` field 106 | in the anonymous class created to act as a context is accessible. Note that the use of non-public 107 | fields will not work in a sandboxed security environment. 108 | 109 | Sections behave as you would expect: 110 | 111 | * `Boolean` values enable or disable the section. 112 | * Array, `Iterator`, or `Iterable` values repeatedly execute the section with each element used as 113 | the context for each iteration. Empty collections result in zero instances of the section being 114 | included in the template. 115 | * An unresolvable or null value is treated as false. This behavior can be changed by using 116 | `strictSections()`. See _Default Values_ for more details. 117 | * Any other object results in a single execution of the section with that object as a context. 118 | 119 | See the code in 120 | [MustacheTest.java](http://github.com/samskivert/jmustache/blob/master/src/test/java/com/samskivert/mustache/MustacheTest.java) 121 | for concrete examples. See also the 122 | [Mustache documentation](http://mustache.github.io/mustache.5.html) for details on the template 123 | syntax. 124 | 125 | Partials 126 | -------- 127 | 128 | If you wish to make use of partials (e.g. `{{>subtmpl}}`) you must provide a 129 | `Mustache.TemplateLoader` to the compiler when creating it. For example: 130 | 131 | ```java 132 | final File templateDir = ...; 133 | Mustache.Compiler c = Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 134 | public Reader getTemplate (String name) { 135 | return new FileReader(new File(templateDir, name)); 136 | } 137 | }); 138 | String tmpl = "...{{>subtmpl}}..."; 139 | c.compile(tmpl).execute(); 140 | ``` 141 | 142 | The above snippet will load `new File(templateDir, "subtmpl")` when compiling the template. 143 | 144 | Lambdas 145 | ------- 146 | 147 | JMustache implements lambdas by passing you a `Template.Fragment` instance which you can use to 148 | execute the fragment of the template that was passed to the lambda. You can decorate the results of 149 | the fragment execution, as shown in the standard Mustache documentation on lambdas: 150 | 151 | ```java 152 | String tmpl = "{{#bold}}{{name}} is awesome.{{/bold}}"; 153 | Mustache.compiler().compile(tmpl).execute(new Object() { 154 | String name = "Willy"; 155 | Mustache.Lambda bold = new Mustache.Lambda() { 156 | public void execute (Template.Fragment frag, Writer out) throws IOException { 157 | out.write(""); 158 | frag.execute(out); 159 | out.write(""); 160 | } 161 | }; 162 | }); 163 | // result: 164 | Willy is awesome. 165 | ``` 166 | 167 | You can also obtain the results of the fragment execution to do things like internationalization or 168 | caching: 169 | 170 | ```java 171 | Object ctx = new Object() { 172 | Mustache.Lambda i18n = new Mustache.Lambda() { 173 | public void execute (Template.Fragment frag, Writer out) throws IOException { 174 | String key = frag.execute(); 175 | String text = // look up key in i18n system 176 | out.write(text); 177 | } 178 | }; 179 | }; 180 | // template might look something like: 181 |

{{#i18n}}title{{/i18n}}

182 | {{#i18n}}welcome_msg{{/i18n}} 183 | ``` 184 | 185 | There is also limited support for decompiling (unexecuting) the template and obtaining the original 186 | Mustache template text contained in the section. See the documentation for [Template.Fragment] for 187 | details on the limitations. 188 | 189 | Default Values 190 | -------------- 191 | 192 | By default, an exception will be thrown any time a variable cannot be resolved, or resolves to null 193 | (except for sections, see below). You can change this behavior in two ways. If you want to provide a 194 | value for use in all such circumstances, use `defaultValue()`: 195 | 196 | ```java 197 | String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?"; 198 | Mustache.compiler().defaultValue("what").compile(tmpl).execute(new Object() { 199 | String exists = "Say"; 200 | String nullValued = null; 201 | // String doesNotExist 202 | }); 203 | // result: 204 | Say what what? 205 | ``` 206 | 207 | If you only wish to provide a default value for variables that resolve to null, and wish to 208 | preserve exceptions in cases where variables cannot be resolved, use `nullValue()`: 209 | 210 | ```java 211 | String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?"; 212 | Mustache.compiler().nullValue("what").compile(tmpl).execute(new Object() { 213 | String exists = "Say"; 214 | String nullValued = null; 215 | // String doesNotExist 216 | }); 217 | // throws MustacheException when executing the template because doesNotExist cannot be resolved 218 | ``` 219 | 220 | When using a `Map` as a context, `nullValue()` will only be used when the map contains a mapping to 221 | `null`. If the map lacks a mapping for a given variable, then it is considered unresolvable and 222 | throws an exception. 223 | 224 | ```java 225 | Map map = new HashMap(); 226 | map.put("exists", "Say"); 227 | map.put("nullValued", null); 228 | // no mapping exists for "doesNotExist" 229 | String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?"; 230 | Mustache.compiler().nullValue("what").compile(tmpl).execute(map); 231 | // throws MustacheException when executing the template because doesNotExist cannot be resolved 232 | ``` 233 | 234 | **Do not** use both `defaultValue` and `nullValue` in your compiler configuration. Each one 235 | overrides the other, so whichever one you call last is the behavior you will get. But even if you 236 | accidentally do the right thing, you have confusing code, so don't call both, use one or the other. 237 | 238 | ### Sections 239 | 240 | Sections are not affected by the `nullValue()` or `defaultValue()` settings. Their behavior is 241 | governed by a separate configuration: `strictSections()`. 242 | 243 | By default, a section that is not resolvable or which resolves to `null` will be omitted (and 244 | conversely, an inverse section that is not resolvable or resolves to `null` will be included). If 245 | you use `strictSections(true)`, sections that refer to an unresolvable value will always throw an 246 | exception. Sections that refer to a resolvable but `null` value never throw an exception, 247 | regardless of the `strictSections()` setting. 248 | 249 | Extensions 250 | ========== 251 | 252 | JMustache extends the basic Mustache template language with some additional functionality. These 253 | additional features are enumerated below: 254 | 255 | Not escaping HTML by default 256 | ---------------------------- 257 | 258 | You can change the default HTML escaping behavior when obtaining a compiler: 259 | 260 | ```java 261 | Mustache.compiler().escapeHTML(false).compile("{{foo}}").execute(new Object() { 262 | String foo = ""; 263 | }); 264 | // result: 265 | // not: <bar> 266 | ``` 267 | 268 | User-defined object formatting 269 | ------------------------------ 270 | 271 | By default, JMustache uses `String.valueOf` to convert objects to strings when rendering a 272 | template. You can customize this formatting by implementing the `Mustache.Formatter` interface: 273 | 274 | ```java 275 | Mustache.compiler().withFormatter(new Mustache.Formatter() { 276 | public String format (Object value) { 277 | if (value instanceof Date) return _fmt.format((Date)value); 278 | else return String.valueOf(value); 279 | } 280 | protected DateFormat _fmt = new SimpleDateFormat("yyyy/MM/dd"); 281 | }).compile("{{msg}}: {{today}}").execute(new Object() { 282 | String msg = "Date"; 283 | Date today = new Date(); 284 | }) 285 | // result: Date: 2013/01/08 286 | ``` 287 | 288 | User-defined escaping rules 289 | --------------------------- 290 | 291 | You can change the escaping behavior when obtaining a compiler, to support file formats other than 292 | HTML and plain text. 293 | 294 | If you only need to replace fixed strings in the text, you can use `Escapers.simple`: 295 | 296 | ```java 297 | String[][] escapes = {{ "[", "[[" }, { "]", "]]" }}; 298 | Mustache.compiler().withEscaper(Escapers.simple(escapes)). 299 | compile("{{foo}}").execute(new Object() { 300 | String foo = "[bar]"; 301 | }); 302 | // result: [[bar]] 303 | ``` 304 | 305 | Or you can implement the `Mustache.Escaper` interface directly for more control over the escaping 306 | process. 307 | 308 | Special variables 309 | ----------------- 310 | 311 | ### this 312 | You can use the special variable `this` to refer to the context object itself instead of one of its 313 | members. This is particularly useful when iterating over lists. 314 | 315 | ```java 316 | Mustache.compiler().compile("{{this}}").execute("hello"); // returns: hello 317 | Mustache.compiler().compile("{{#names}}{{this}}{{/names}}").execute(new Object() { 318 | List names () { return Arrays.asList("Tom", "Dick", "Harry"); } 319 | }); 320 | // result: TomDickHarry 321 | ``` 322 | 323 | Note that you can also use the special variable `.` to mean the same thing. 324 | 325 | ```java 326 | Mustache.compiler().compile("{{.}}").execute("hello"); // returns: hello 327 | Mustache.compiler().compile("{{#names}}{{.}}{{/names}}").execute(new Object() { 328 | List names () { return Arrays.asList("Tom", "Dick", "Harry"); } 329 | }); 330 | // result: TomDickHarry 331 | ``` 332 | 333 | `.` is apparently supported by other Mustache implementations, though it does not appear in the 334 | official documentation. 335 | 336 | ### -first and -last 337 | You can use the special variables `-first` and `-last` to perform special processing for list 338 | elements. `-first` resolves to `true` when inside a section that is processing the first of a list 339 | of elements. It resolves to `false` at all other times. `-last` resolves to `true` when inside a 340 | section that is processing the last of a list of elements. It resolves to `false` at all other 341 | times. 342 | 343 | One will often make use of these special variables in an inverted section, as follows: 344 | 345 | ```java 346 | String tmpl = "{{#things}}{{^-first}}, {{/-first}}{{this}}{{/things}}"; 347 | Mustache.compiler().compile(tmpl).execute(new Object() { 348 | List things = Arrays.asList("one", "two", "three"); 349 | }); 350 | // result: one, two, three 351 | ``` 352 | 353 | Note that the values of `-first` and `-last` refer only to the inner-most enclosing section. If you 354 | are processing a section within a section, there is no way to find out whether you are in the first 355 | or last iteration of an outer section. 356 | 357 | ### -index 358 | The `-index` special variable contains 1 for the first iteration through a section, 2 for the 359 | second, 3 for the third and so forth. It contains 0 at all other times. Note that it also contains 360 | 0 for a section that is populated by a singleton value rather than a list. 361 | 362 | ```java 363 | String tmpl = "My favorite things:\n{{#things}}{{-index}}. {{this}}\n{{/things}}"; 364 | Mustache.compiler().compile(tmpl).execute(new Object() { 365 | List things = Arrays.asList("Peanut butter", "Pen spinning", "Handstands"); 366 | }); 367 | // result: 368 | // My favorite things: 369 | // 1. Peanut butter 370 | // 2. Pen spinning 371 | // 3. Handstands 372 | ``` 373 | 374 | Compound variables 375 | ------------------ 376 | 377 | In addition to resolving simple variables using the context, you can use compound variables to 378 | extract data from sub-objects of the current context. For example: 379 | 380 | ```java 381 | Mustache.compiler().compile("Hello {{field.who}}!").execute(new Object() { 382 | public Object field = new Object() { 383 | public String who () { return "world"; } 384 | } 385 | }); 386 | // result: Hello world! 387 | ``` 388 | 389 | By taking advantage of reflection and bean-property-style lookups, you can do kooky things: 390 | 391 | ```java 392 | Mustache.compiler().compile("Hello {{class.name}}!").execute(new Object()); 393 | // result: Hello java.lang.Object! 394 | ``` 395 | 396 | Note that compound variables are essentially short-hand for using singleton sections. The above 397 | examples could also be represented as: 398 | 399 | Hello {{#field}}{{who}}{{/field}}! 400 | Hello {{#class}}{{name}}{{/class}}! 401 | 402 | Note also that one semantic difference exists between nested singleton sections and compound 403 | variables: after resolving the object for the first component of the compound variable, parent 404 | contexts will not be searched when resolving subcomponents. 405 | 406 | Newline trimming 407 | ---------------- 408 | 409 | If the opening or closing section tag are the only thing on a line, any surrounding whitespace and 410 | the line terminator following the tag are trimmed. This allows for civilized templates, like: 411 | 412 | ```html 413 | Favorite foods: 414 |
    415 | {{#people}} 416 |
  • {{first_name}} {{last_name}} likes {{favorite_food}}.
  • 417 | {{/people}} 418 |
419 | ``` 420 | 421 | which produces output like: 422 | 423 | ```html 424 | Favorite foods: 425 |
    426 |
  • Elvis Presley likes peanut butter.
  • 427 |
  • Mahatma Gandhi likes aloo dum.
  • 428 |
429 | ``` 430 | 431 | rather than: 432 | 433 | ```html 434 | Favorite foods: 435 |
    436 | 437 |
  • Elvis Presley likes peanut butter.
  • 438 | 439 |
  • Mahatma Gandhi likes aloo dum.
  • 440 | 441 |
442 | ``` 443 | 444 | which would be produced without the newline trimming. 445 | 446 | Nested Contexts 447 | --------------- 448 | 449 | If a variable is not found in a nested context, it is resolved in the next outer context. This 450 | allows usage like the following: 451 | 452 | ```java 453 | String template = "{{outer}}:\n{{#inner}}{{outer}}.{{this}}\n{{/inner}}"; 454 | Mustache.compiler().compile(template).execute(new Object() { 455 | String outer = "foo"; 456 | List inner = Arrays.asList("bar", "baz", "bif"); 457 | }); 458 | // results: 459 | // foo: 460 | // foo.bar 461 | // foo.baz 462 | // foo.bif 463 | ``` 464 | 465 | Note that if a variable _is_ defined in an inner context, it shadows the same name in the outer 466 | context. There is presently no way to access the variable from the outer context. 467 | 468 | Invertible Lambdas 469 | ------------------ 470 | 471 | For some applications, it may be useful for lambdas to be executed for an inverse section rather 472 | than having the section omitted altogether. This allows for proper conditional substitution when 473 | statically translating templates into other languages or contexts: 474 | 475 | ```java 476 | String template = "{{#condition}}result if true{{/condition}}\n" + 477 | "{{^condition}}result if false{{/condition}}"; 478 | Mustache.compiler().compile(template).execute(new Object() { 479 | Mustache.InvertibleLambda condition = new Mustache.InvertibleLambda() { 480 | public void execute (Template.Fragment frag, Writer out) throws IOException { 481 | // this method is executed when the lambda is referenced in a normal section 482 | out.write("if (condition) {console.log(\""); 483 | out.write(toJavaScriptLiteral(frag.execute())); 484 | out.write("\")}"); 485 | } 486 | public void executeInverse (Template.Fragment frag, Writer out) throws IOException { 487 | // this method is executed when the lambda is referenced in an inverse section 488 | out.write("if (!condition) {console.log(\""); 489 | out.write(toJavaScriptLiteral(frag.execute())); 490 | out.write("\")}"); 491 | } 492 | private String toJavaScriptLiteral (String execute) { 493 | // note: this is NOT a complete implementation of JavaScript string literal escaping 494 | return execute.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\""); 495 | } 496 | }; 497 | }); 498 | // results: 499 | // if (condition) {console.log("result if true")} 500 | // if (!condition) {console.log("result if false")} 501 | ``` 502 | 503 | Of course, you are not limited strictly to conditional substitution -- you can use an 504 | InvertibleLambda whenever you need a single function with two modes of operation. 505 | 506 | Standards Mode 507 | -------------- 508 | 509 | The more intrusive of these extensions, specifically the searching of parent contexts and the use 510 | of compound variables, can be disabled when creating a compiler, like so: 511 | 512 | ```java 513 | Map ctx = new HashMap(); 514 | ctx.put("foo.bar", "baz"); 515 | Mustache.compiler().standardsMode(true).compile("{{foo.bar}}").execute(ctx); 516 | // result: baz 517 | ``` 518 | 519 | Thread Safety 520 | ============= 521 | 522 | JMustache is internally thread safe with the following caveats: 523 | 524 | * Compilation: compiling templates calls out to a variety of helper classes: 525 | `Mustache.Formatter`, `Mustache.Escaper`, `Mustache.TemplateLoader`, `Mustache.Collector`. The 526 | default implementations of these classes are thread-safe, but if you supply custom instances, 527 | then you have to ensure that your custom instances are thread-safe. 528 | 529 | * Execution: executing templates can call out to some helper classes: `Mustache.Lambda`, 530 | `Mustache.VariableFetcher`. The default implementations of these classes are thread-safe, but 531 | if you supply custom instances, then you have to ensure that your custom instances are 532 | thread-safe. 533 | 534 | * Context data: if you mutate the context data passed to template execution while the template is 535 | being executed, then you subject yourself to race conditions. It is in theory possible to use a 536 | thread-safe map (`ConcurrentHashMap` or `Collections.synchronizedMap`) for your context data, 537 | which would allow you to mutate the data while templates were being rendered based on that 538 | data, but you're playing with fire by doing that. I don't recommend it. If your data is 539 | supplied as POJOs where fields or methods are called via reflection to populate your templates, 540 | volatile fields and synchronized methods could similarly be used to support simultaneous 541 | reading and mutating, but again you could easily make a mistake that introduces race conditions 542 | or cause weirdness when executing your templates. The safest approach when rendering the same 543 | template via simultaneous threads is to pass immutable/unchanging data as the context for each 544 | execution. 545 | 546 | * `VariableFetcher` cache: template execution uses one internal cache to store resolved 547 | `VariableFetcher` instances (because resolving a variable fetcher is expensive). This cache is 548 | thread-safe by virtue of using a `ConcurrentHashMap`. It's possible for a bit of extra work to 549 | be done if two threads resolve the same variable at the same time, but they won't conflict with 550 | one another, they'll simply both resolve the variable instead of one resolving the variable and 551 | the other using the cached resolution. 552 | 553 | So the executive summary is: as long as all helper classes you supply are thread-safe (or you use 554 | the defaults), it is safe to share a `Mustache.Compiler` instance across threads to compile 555 | templates. If you pass immutable data to your templates when executing, it is safe to have multiple 556 | threads simultaneously execute a single `Template` instance. 557 | 558 | Limitations 559 | =========== 560 | 561 | In the name of simplicity, some features of Mustache were omitted or simplified: 562 | 563 | * `{{= =}}` only supports one or two character delimiters. This is just because I'm lazy and it 564 | simplifies the parser. 565 | 566 | [Template.Fragment]: http://samskivert.github.io/jmustache/apidocs/com/samskivert/mustache/Template.Fragment.html#decompile-- 567 | -------------------------------------------------------------------------------- /src/test/java/com/samskivert/mustache/SharedTests.java: -------------------------------------------------------------------------------- 1 | // 2 | // JMustache - A Java implementation of the Mustache templating language 3 | // http://github.com/samskivert/jmustache/blob/master/LICENSE 4 | 5 | package com.samskivert.mustache; 6 | 7 | import static java.util.Map.entry; 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertTrue; 10 | import static org.junit.Assert.fail; 11 | 12 | import java.io.IOException; 13 | import java.io.Reader; 14 | import java.io.StringReader; 15 | import java.io.Writer; 16 | import java.util.Arrays; 17 | import java.util.Collections; 18 | import java.util.HashMap; 19 | import java.util.HashSet; 20 | import java.util.LinkedHashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Map.Entry; 24 | import java.util.Objects; 25 | import java.util.Set; 26 | 27 | import org.junit.Ignore; 28 | import org.junit.Rule; 29 | import org.junit.Test; 30 | import org.junit.rules.TestRule; 31 | import org.junit.rules.TestWatcher; 32 | import org.junit.runner.Description; 33 | 34 | import com.samskivert.mustache.specs.SpecTest; 35 | 36 | /** 37 | * Vestige from when JMustache supported both GWT and JVM. 38 | */ 39 | public abstract class SharedTests 40 | { 41 | @Test public void testSimpleVariable () { 42 | test("bar", "{{foo}}", context("foo", "bar")); 43 | } 44 | 45 | @Test public void testPrimitiveArrayVariable () { 46 | test("1234", "{{#foo}}{{this}}{{/foo}}", context("foo", new int[] { 1, 2, 3, 4 })); 47 | } 48 | 49 | @Test public void testPrimitiveArrayIndexVariable () { 50 | test("1", "{{foo.0}}", context("foo", new int[] { 1, 2, 3, 4 })); 51 | } 52 | 53 | @Test public void testPrimitiveArrayIndexOutOfBoundsVariable () { 54 | Mustache.Compiler comp = Mustache.compiler().defaultValue("?"); 55 | test(comp, "?", "{{foo.4}}", context("foo", new int[] { 1, 2, 3, 4 })); 56 | } 57 | 58 | @Test public void testOneShotSection () { 59 | test("baz", "{{#foo}}{{bar}}{{/foo}}", context("foo", context("bar", "baz"))); 60 | } 61 | 62 | @Test public void testListSection () { 63 | test("bazbif", "{{#foo}}{{bar}}{{/foo}}", context( 64 | "foo", Arrays.asList(context("bar", "baz"), context("bar", "bif")))); 65 | } 66 | 67 | @Test public void testListIndexSection() { 68 | test("baz", "{{#foo.0}}{{bar}}{{/foo.0}}", context( 69 | "foo", Arrays.asList(context("bar", "baz"), context("bar", "bif")))); 70 | } 71 | 72 | @Test public void testListIndexOutOfBoundsSection () { 73 | test("", "{{#foo.2}}{{bar}}{{/foo.2}}", context( 74 | "foo", Arrays.asList(context("bar", "baz"), context("bar", "bif")))); 75 | } 76 | 77 | @Test public void testListItemSection () { 78 | test("baz", "{{foo.0.bar}}", context( 79 | "foo", Arrays.asList(context("bar", "baz"), context("bar", "bif")))); 80 | } 81 | 82 | @Test public void testMapEntriesSection () { 83 | Map data = new HashMap(); 84 | data.put("k1", "v1"); 85 | data.put("k2", "v2"); 86 | test(Mustache.compiler().escapeHTML(false), 87 | "k1=v1k2=v2", "{{#map.entrySet}}{{.}}{{/map.entrySet}}", 88 | context("map", data)); 89 | } 90 | 91 | @Test public void testArraySection () { 92 | test("bazbif", "{{#foo}}{{bar}}{{/foo}}", 93 | context("foo", new Object[] { context("bar", "baz"), context("bar", "bif") })); 94 | } 95 | 96 | @Test public void testArrayIndexSection () { 97 | test("baz", "{{#foo.0}}{{bar}}{{/foo.0}}", 98 | context("foo", new Object[] { 99 | context("bar", "baz"), context("bar", "bif") })); 100 | } 101 | 102 | @Test public void testArrayIndexOutOfBoundsSection () { 103 | test("", "{{#foo.2}}{{bar}}{{/foo.2}}", 104 | context("foo", new Object[] { 105 | context("bar", "baz"), context("bar", "bif") })); 106 | } 107 | 108 | @Test public void testArrayItemSection () { 109 | test("baz", "{{foo.0.bar}}", 110 | context("foo", new Object[] { 111 | context("bar", "baz"), context("bar", "bif") })); 112 | } 113 | 114 | @Test public void testIteratorSection () { 115 | test("bazbif", "{{#foo}}{{bar}}{{/foo}}", 116 | context("foo", Arrays.asList(context("bar", "baz"), 117 | context("bar", "bif")).iterator())); 118 | } 119 | 120 | @Test public void testIteratorIndexSection () { 121 | test("baz", "{{#foo.0}}{{bar}}{{/foo.0}}", 122 | context("foo", Arrays.asList(context("bar", "baz"), 123 | context("bar", "bif")).iterator())); 124 | } 125 | 126 | @Test public void testIteratorIndexOutOfBoundsSection () { 127 | test("", "{{#foo.2}}{{bar}}{{/foo.2}}", 128 | context("foo", Arrays.asList(context("bar", "baz"), 129 | context("bar", "bif")).iterator())); 130 | } 131 | 132 | @Test public void testIteratorItemSection () { 133 | test("baz", "{{foo.0.bar}}", 134 | context("foo", Arrays.asList(context("bar", "baz"), 135 | context("bar", "bif")).iterator())); 136 | } 137 | 138 | @Test public void testEmptyListSection () { 139 | test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", Collections.emptyList())); 140 | } 141 | 142 | @Test public void testEmptyArraySection () { 143 | test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", new Object[0])); 144 | } 145 | 146 | @Test public void testEmptyIteratorSection () { 147 | test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", Collections.emptyList().iterator())); 148 | } 149 | 150 | @Test public void testFalseSection () { 151 | test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", false)); 152 | } 153 | 154 | @Test public void testNestedListSection () { 155 | test("1234", "{{#a}}{{#b}}{{c}}{{/b}}{{#d}}{{e}}{{/d}}{{/a}}", 156 | context("a", context("b", new Object[] { context("c", "1"), context("c", "2") }, 157 | "d", new Object[] { context("e", "3"), context("e", "4") }))); 158 | } 159 | 160 | @Test public void testNullSection () { 161 | Object ctx = context("foo", null); 162 | test("", "{{#foo}}{{bar}}{{/foo}}", ctx); 163 | test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", ctx); 164 | test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", ctx); 165 | } 166 | 167 | @Test public void testMissingNonStrictSection () { 168 | // no foo; section omitted due to non-strict-sections 169 | test("", "{{#foo}}{{bar}}{{/foo}}", EMPTY); 170 | // no foo; no exception because nullValue does change section strictness 171 | test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", EMPTY); 172 | // no foo; no exception because defaultValue does change section strictness 173 | test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", EMPTY); 174 | } 175 | 176 | @Test public void testMissingStrictSection () { 177 | try { 178 | test(Mustache.compiler().strictSections(true), "", "{{#foo}}{{bar}}{{/foo}}", 179 | EMPTY); // no foo; should throw exception due to strict-sections 180 | fail(); 181 | } catch (MustacheException me) {} // expected 182 | } 183 | 184 | @Test public void testMissingStrictSectionNullValue () { 185 | try { 186 | // missing strict-sections always throw regardless of nullValue() 187 | test(Mustache.compiler().strictSections(true).nullValue(""), "", 188 | "{{#foo}}{{bar}}{{/foo}}", EMPTY); 189 | fail(); 190 | } catch (MustacheException me) {} // expected 191 | } 192 | 193 | @Test public void testMissingStrictSectionDefaultValue () { 194 | try { 195 | // missing strict-sections always throw regardless of defaultValue() 196 | test(Mustache.compiler().strictSections(true).defaultValue(""), "", 197 | "{{#foo}}{{bar}}{{/foo}}", EMPTY); 198 | fail(); 199 | } catch (MustacheException me) {} // expected 200 | } 201 | 202 | @Test public void testSectionWithNonFalseyEmptyString () { 203 | test(Mustache.compiler(), "test", "{{#foo}}test{{/foo}}", context("foo", "")); 204 | } 205 | 206 | @Test public void testSectionWithFalseyEmptyString () { 207 | Object ctx = context("foo", "", "bar", "nonempty"); 208 | // test normal sections with falsey empty string 209 | Mustache.Compiler compiler = Mustache.compiler().emptyStringIsFalse(true); 210 | test(compiler, "", "{{#foo}}test{{/foo}}", ctx); 211 | test(compiler, "test", "{{#bar}}test{{/bar}}", ctx); 212 | // test inverted sections with falsey empty string 213 | test(compiler, "test", "{{^foo}}test{{/foo}}", ctx); 214 | test(compiler, "", "{{^bar}}test{{/bar}}", ctx); 215 | } 216 | 217 | @Test public void testComment () { 218 | test("foobar", "foo{{! nothing to see here}}bar", EMPTY); 219 | } 220 | 221 | @Test public void testCommentWithFunnyChars() { 222 | test("foobar", "foo{{! {baz\n }}bar", EMPTY); 223 | } 224 | 225 | @Test public void testPartialUseWhenUnconfigured () { 226 | try { 227 | test(null, "{{>foo}}", null); 228 | fail(); 229 | } catch (UnsupportedOperationException uoe) {} // expected 230 | } 231 | 232 | Map partials = new LinkedHashMap<>(); 233 | 234 | @SafeVarargs 235 | protected final Mustache.TemplateLoader partials(Map.Entry ... entries) { 236 | Map templates = new LinkedHashMap<>(); 237 | for (Entry e : entries) { 238 | templates.put(e.getKey(), e.getValue()); 239 | } 240 | partials = templates; 241 | return name -> new StringReader(templates.get(name)); 242 | } 243 | 244 | @Test public void testPartial () { 245 | test(Mustache.compiler().withLoader( 246 | partials(entry("foo", "inside:{{bar}}"), 247 | entry("baz", "nonfoo"))) 248 | , "foo inside:foo nonfoo foo", "{{bar}} {{>foo}} {{>baz}} {{bar}}", context("bar", "foo")); 249 | } 250 | 251 | @Test public void testPartialIndent () { 252 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 253 | public Reader getTemplate (String name) { 254 | return new StringReader("|\n{{{content}}}\n|\n"); 255 | } 256 | }), "\\\n |\n <\n->\n |\n/\n", "\\\n {{>partial}}\n/\n", context("content", "<\n->")); 257 | } 258 | 259 | @Test public void testPartialBlankLines () { 260 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 261 | public Reader getTemplate (String name) { 262 | return new StringReader("|\na\n\nb\n|\n"); 263 | } 264 | }), "\\\n\t|\n\ta\n\t\n\tb\n\t|\n/\n", "\\\n\t{{>partial}}\n/\n", context()); 265 | } 266 | 267 | @Test public void testNestedPartialBlankLines () { 268 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 269 | public Reader getTemplate (String name) { 270 | if (name.equals("partial")) { 271 | return new StringReader("1\n\t{{>nest}}\n1\n"); 272 | } else { 273 | return new StringReader("2\na\n\nb\n2\n"); 274 | } 275 | } 276 | }), "\\\n\t1\n\t\t2\n\t\ta\n\t\t\n\t\tb\n\t\t2\n\t1\n/\n", "\\\n\t{{>partial}}\n/\n", context()); 277 | } 278 | 279 | @Test public void testNestedPartialBlankLinesCRLF () { 280 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 281 | public Reader getTemplate (String name) { 282 | if (name.equals("partial")) { 283 | return new StringReader("1\r\n\t{{>nest}}\r\n1\r\n"); 284 | } else { 285 | return new StringReader("2\r\na\r\n\r\nb\r\n2\r\n"); 286 | } 287 | } 288 | }), "\\\r\n\t1\r\n\t\t2\r\n\t\ta\r\n\t\t\r\n\t\tb\r\n\t\t2\r\n\t1\r\n/\r\n", "\\\r\n\t{{>partial}}\r\n/\r\n", context()); 289 | } 290 | 291 | @Test public void testNestedPartialIndent () { 292 | Mustache.TemplateLoader loader = partials(entry("partial", "1\n {{>nest}}\n1\n"), entry("nest", "2\n{{{content}}}\n2\n")); 293 | test(Mustache.compiler().withLoader(loader), 294 | "|\n 1\n 2\n <\n->\n 2\n 1\n|\n", 295 | "|\n {{>partial}}\n|\n", context("content", "<\n->")); 296 | } 297 | 298 | @Test public void testPartialIndentWithVariableAtTheStart () { 299 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 300 | public Reader getTemplate (String name) { 301 | return new StringReader("{{{content}}}\n|\n"); 302 | } 303 | }), "\\\n <\n->\n |\n/\n", "\\\n {{>partial}}\n/\n", context("content", "<\n->")); 304 | } 305 | 306 | @Test public void testPartialIndentWithBlock () { 307 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 308 | public Reader getTemplate (String name) { 309 | return new StringReader("|\n{{#show}}\n{{{content}}}{{/show}}\n|\n"); 310 | } 311 | }), "\\\n |\n <\n->\n |\n/\n", "\\\n {{>partial}}\n/\n", context("show", true, "content", "<\n->")); 312 | } 313 | @Test public void testPartialIndentInBlock () { 314 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 315 | public Reader getTemplate (String name) { 316 | return new StringReader("content"); 317 | } 318 | }), " content", "{{#show}}\n {{>partial}}\n{{/show}}\n", context("show", true)); 319 | } 320 | 321 | @Test public void testPartialIndentWithBlockAtStart () { 322 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 323 | public Reader getTemplate (String name) { 324 | return new StringReader("{{#show}}\n{{{content}}}{{/show}}\n|\n"); 325 | } 326 | }), "\\\n <\n->\n |\n/\n", "\\\n {{>partial}}\n/\n", context("show", true, "content", "<\n->")); 327 | } 328 | 329 | @Test public void testPartialIndentWithInlineBlock () { 330 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 331 | public Reader getTemplate (String name) { 332 | return new StringReader("line {{#show}}content{{/show}}\n"); 333 | } 334 | }), "\\\n line content\n/\n", "\\\n {{>partial}}\n/\n", context("show", true)); 335 | } 336 | 337 | @Test public void testPartialPlusNestedContext () { 338 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 339 | public Reader getTemplate (String name) { 340 | if (name.equals("nested")) { 341 | return new StringReader("{{name}}{{thing_name}}"); 342 | } else { 343 | return new StringReader("nonfoo"); 344 | } 345 | } 346 | }), "foo((foobar)(foobaz))", "{{name}}({{#things}}({{>nested}}){{/things}})", 347 | context("name", "foo", 348 | "things", Arrays.asList(context("thing_name", "bar"), 349 | context("thing_name", "baz")))); 350 | } 351 | 352 | @Test public void testRecursivePartial () { 353 | String template = "[{{name}}{{#properties}}, {{> schema.mustache}}{{/properties}}]"; 354 | test(Mustache.compiler().withLoader(new Mustache.TemplateLoader() { 355 | public Reader getTemplate (String name) { 356 | return new StringReader(template); 357 | } 358 | }), "[level0, [level1a, [level2a]], [level1b, [level2b]]]", template, 359 | context("name", "level0", 360 | "properties", Arrays.asList( 361 | context("name", "level1a", 362 | // test with empty list as recursive terminator 363 | "properties", context("name", "level2a", "properties", Arrays.asList())), 364 | context("name", "level1b", 365 | // test with null as recursive terminator 366 | "properties", context("name", "level2b", "properties", null)) 367 | ))); 368 | } 369 | 370 | @Test public void testDelimiterChange () { 371 | test("foo bar baz", "{{one}} {{=<% %>=}}<%two%><%={{ }}=%> {{three}}", 372 | context("one", "foo", "two", "bar", "three", "baz")); 373 | test("baz bar foo", "{{three}} {{=% %=}}%two%%={{ }}=% {{one}}", 374 | context("one", "foo", "two", "bar", "three", "baz")); 375 | } 376 | 377 | @Test public void testUnescapeHTML () { 378 | check("", Mustache.compiler().escapeHTML(true).compile("{{&a}}"). 379 | execute(context("a", ""))); 380 | check("", Mustache.compiler().escapeHTML(true).compile("{{{a}}}"). 381 | execute(context("a", ""))); 382 | // make sure these also work when escape HTML is off 383 | check("", Mustache.compiler().escapeHTML(false).compile("{{&a}}"). 384 | execute(context("a", ""))); 385 | check("", Mustache.compiler().escapeHTML(false).compile("{{{a}}}"). 386 | execute(context("a", ""))); 387 | } 388 | 389 | @Test public void testDanglingTag () { 390 | test("foo{", "foo{", context("a", "")); 391 | test("foo{{", "foo{{", context("a", "")); 392 | test("foo{{a", "foo{{a", context("a", "")); 393 | test("foo{{a}", "foo{{a}", context("a", "")); 394 | } 395 | 396 | @Test public void testStrayTagCharacters () { 397 | test("funny [b] business {{", "funny {{a}} business {{", context("a", "[b]")); 398 | test("funny business {{", "funny {{{a}}} business {{", context("a", "")); 399 | test("{{ funny [b] business", "{{ funny {{a}} business", context("a", "[b]")); 400 | test("{{ funny business", "{{ funny {{{a}}} business", context("a", "")); 401 | test("funny [b] business }}", "funny {{a}} business }}", context("a", "[b]")); 402 | test("funny business }}", "funny {{{a}}} business }}", context("a", "")); 403 | test("}} funny [b] business", "}} funny {{a}} business", context("a", "[b]")); 404 | test("}} funny business", "}} funny {{{a}}} business", context("a", "")); 405 | } 406 | 407 | @Test public void testInvalidUnescapeHTML () { 408 | try { 409 | Mustache.compiler().escapeHTML(true).compile("{{{a}}").execute(context("a", "")); 410 | fail(); 411 | } catch (MustacheParseException me) {} // expected 412 | } 413 | 414 | @Test public void testEscapeHTML () { 415 | check("<b>", Mustache.compiler().compile("{{a}}").execute(context("a", ""))); 416 | check("", Mustache.compiler().escapeHTML(false).compile("{{a}}"). 417 | execute(context("a", ""))); 418 | // ensure that some potential XSS enablers are escaped 419 | check("`=", Mustache.compiler().compile("{{a}}").execute(context("a", "`="))); 420 | } 421 | 422 | @Test public void testUserDefinedEscaping() { 423 | Mustache.Escaper escaper = Escapers.simple(new String[][] { 424 | { "[", ":BEGIN:" }, 425 | { "]", ":END:" } 426 | }); 427 | check(":BEGIN:b:END:", Mustache.compiler().withEscaper(escaper). 428 | compile("{{a}}").execute(context("a", "[b]"))); 429 | } 430 | 431 | @Test public void testPartialDelimiterMatch () { 432 | check("{bob}", Mustache.compiler().compile("{bob}").execute(EMPTY)); 433 | check("bar", Mustache.compiler().compile("{{bob}bob}}").execute(context("bob}bob", "bar"))); 434 | } 435 | 436 | @Test public void testTopLevelThis () { 437 | check("bar", Mustache.compiler().compile("{{this}}").execute("bar")); 438 | check("bar", Mustache.compiler().compile("{{.}}").execute("bar")); 439 | } 440 | 441 | @Test public void testNestedThis () { 442 | check("barbazbif", Mustache.compiler().compile("{{#things}}{{this}}{{/things}}"). 443 | execute(context("things", Arrays.asList("bar", "baz", "bif")))); 444 | check("barbazbif", Mustache.compiler().compile("{{#things}}{{.}}{{/things}}"). 445 | execute(context("things", Arrays.asList("bar", "baz", "bif")))); 446 | } 447 | 448 | @Test public void testNestedNullThis () { 449 | check("bar!bif", Mustache.compiler().defaultValue("!"). 450 | compile("{{#things}}{{.}}{{/things}}"). 451 | execute(context("things", Arrays.asList("bar", null, "bif")))); 452 | } 453 | 454 | @Test public void testNewlineSkipping () { 455 | testNewlineSkipping("\n"); 456 | } 457 | 458 | @Test public void testNewlineSkippingCRLF () { 459 | testNewlineSkipping("\r\n"); 460 | } 461 | 462 | @Test public void testNewlineSkippingDelimsTag () { 463 | test("Begin.\nEnd.\n", "Begin.\n{{=@ @=}}\nEnd.\n", EMPTY); 464 | } 465 | 466 | @Test public void testNoTrimNewlineFromNestedTagAt0 () { 467 | test(" | \n | \n", " | {{^boolean}}{{! comment }}\n {{/boolean}} | \n", 468 | context("boolean", false)); 469 | } 470 | 471 | @Test public void testTrimBlank () { 472 | Mustache.StringSegment str = new Mustache.StringSegment(" \r\n ", false); 473 | check("Text( )-1/0", str.trimLeadBlank().toString()); 474 | check("Text( \\r\\n)3/-1", str.trimTrailBlank().toString()); 475 | } 476 | 477 | static class GetKeysVisitor implements Mustache.Visitor { 478 | public final Set keys = new HashSet(); 479 | public void visitText (String text) {} 480 | public void visitVariable (String name) { keys.add(name); } 481 | public boolean visitInclude (String name) { keys.add(name); return true; } 482 | public boolean visitSection (String name) { keys.add(name); return true; } 483 | public boolean visitInvertedSection (String name) { keys.add(name); return true; } 484 | } 485 | 486 | @Test public void testVisit() { 487 | String template = "{{#one}}1{{/one}} {{^two}}2{{three}}{{/two}}{{four}}"; 488 | GetKeysVisitor viz = new GetKeysVisitor(); 489 | Mustache.compiler().compile(template).visit(viz); 490 | List expect = Arrays.asList("one", "two", "three", "four"); 491 | assertEquals(new HashSet<>(expect), viz.keys); 492 | } 493 | 494 | @Test public void testVisitNested() { 495 | String template = "{{#one}}1{{/one}} {{^two}}" + 496 | "2{{two_and_half}}{{#five}}{{three}}{{/five}}" + 497 | "{{/two}} {{#one}}{{three}}{{/one}} {{four}}{{! ignore me }}"; 498 | GetKeysVisitor viz = new GetKeysVisitor(); 499 | Mustache.compiler().compile(template).visit(viz); 500 | List expect = Arrays.asList("one", "two", "three", "four", "two_and_half", "five"); 501 | assertEquals(new HashSet<>(expect), viz.keys); 502 | } 503 | 504 | protected void testNewlineSkipping (String sep) { 505 | String tmpl = "list:" + sep + 506 | "{{#items}}" + sep + 507 | "{{this}}" + sep + 508 | "{{/items}}" + sep + 509 | "{{^items}}" + sep + 510 | "no items" + sep + 511 | "{{/items}}" + sep + 512 | "endlist"; 513 | test("list:" + sep + 514 | "one" + sep + 515 | "two" + sep + 516 | "three" + sep + 517 | "endlist", tmpl, context("items", Arrays.asList("one", "two", "three"))); 518 | test("list:" + sep + 519 | "no items" + sep + 520 | "endlist", tmpl, context("items", Collections.emptyList())); 521 | 522 | // this tests newline trimming even if the group tags have leading/trailing whitespace 523 | String htmlTmpl = 524 | "
    " + sep + 525 | " {{#items}} " + sep + 526 | "
  • {{this}}
  • " + sep + 527 | " {{/items}} " + sep + 528 | " {{^items}}" + sep + 529 | "
  • no items
  • " + sep + 530 | " {{/items}}" + sep + 531 | "
"; 532 | test("
    " + sep + 533 | "
  • one
  • " + sep + 534 | "
  • two
  • " + sep + 535 | "
  • three
  • " + sep + 536 | "
", htmlTmpl, context("items", Arrays.asList("one", "two", "three"))); 537 | test("
    " + sep + 538 | "
  • no items
  • " + sep + 539 | "
", htmlTmpl, context("items", Collections.emptyList())); 540 | } 541 | 542 | @Test public void testNewlineNonSkipping () { 543 | // only when a section tag is by itself on a line should we absorb the newline following it 544 | String tmpl = "thing?: {{#thing}}yes{{/thing}}{{^thing}}no{{/thing}}\n" + 545 | "that's nice"; 546 | test("thing?: yes\n" + 547 | "that's nice", tmpl, context("thing", true)); 548 | test("thing?: no\n" + 549 | "that's nice", tmpl, context("thing", false)); 550 | } 551 | 552 | @Test public void testNestedContexts () { 553 | test("foo((foobar)(foobaz))", "{{name}}({{#things}}({{name}}{{thing_name}}){{/things}})", 554 | context("name", "foo", 555 | "things", Arrays.asList(context("thing_name", "bar"), 556 | context("thing_name", "baz")))); 557 | } 558 | 559 | @Test public void testShadowedContext () { 560 | test("foo((bar)(baz))", "{{name}}({{#things}}({{name}}){{/things}})", 561 | context("name", "foo", 562 | "things", Arrays.asList(context("name", "bar"), context("name", "baz")))); 563 | } 564 | 565 | @Test public void testFirst () { 566 | test("foo|bar|baz", "{{#things}}{{^-first}}|{{/-first}}{{this}}{{/things}}", 567 | context("things", Arrays.asList("foo", "bar", "baz"))); 568 | } 569 | 570 | @Test public void testLast () { 571 | test("foo|bar|baz", "{{#things}}{{this}}{{^-last}}|{{/-last}}{{/things}}", 572 | context("things", Arrays.asList("foo", "bar", "baz"))); 573 | } 574 | 575 | @Test public void testFirstLast () { 576 | test("[foo]", "{{#things}}{{#-first}}[{{/-first}}{{this}}{{#-last}}]{{/-last}}{{/things}}", 577 | context("things", Arrays.asList("foo"))); 578 | test("foo", "{{#things}}{{this}}{{^-last}}|{{/-last}}{{/things}}", 579 | context("things", Arrays.asList("foo"))); 580 | } 581 | 582 | @Test public void testFirstLastCombo () { 583 | test("FIRST_and_LAST", "{{#things}}{{#-first}}FIRST{{/-first}}{{this}}{{#-last}}LAST{{/-last}}{{/things}}", 584 | context("things", Arrays.asList("_and_"))); 585 | } 586 | 587 | @Test public void testInverseFirstLastCombo () { 588 | test("_and_", "{{#things}}{{^-first}}NOT-FIRST{{/-first}}{{this}}{{^-last}}NOT-LAST{{/-last}}{{/things}}", 589 | context("things", Arrays.asList("_and_"))); 590 | } 591 | 592 | @Test public void testNotFirst () { 593 | test("1,2,3", "{{#things}}{{^-first}},{{/-first}}{{this}}{{/things}}", 594 | context("things", Arrays.asList("1", "2", "3"))); 595 | } 596 | 597 | @Test public void testNotLast () { 598 | test("1,2,3", "{{#things}}{{this}}{{^-last}},{{/-last}}{{/things}}", 599 | context("things", Arrays.asList("1", "2", "3"))); 600 | } 601 | 602 | @Test public void testIndex () { 603 | test("123", "{{#things}}{{-index}}{{/things}}", 604 | context("things", Arrays.asList("foo", "bar", "baz"))); 605 | } 606 | 607 | @Test public void testNestedIndex () { 608 | String tmpl = 609 | "{{#fooList}}\n" + 610 | "{{#quantity}}|q{{-index}}={{quantity}}{{/quantity}}|{{name}}\n" + 611 | "{{/fooList}}"; 612 | test("|q1=1|a\n|b\n", tmpl, 613 | context("fooList", Arrays.asList(context("name", "a", "quantity", 1), 614 | context("name", "b")))); 615 | } 616 | 617 | @Test public void testLineReporting () { 618 | String tmpl = "first line\n{{nonexistent}}\nsecond line"; 619 | try { 620 | Mustache.compiler().compile(tmpl).execute(EMPTY); 621 | fail("Referencing a nonexistent variable should throw MustacheException"); 622 | } catch (MustacheException e) { 623 | assertTrue(e.getMessage().contains("line 2")); 624 | } 625 | } 626 | 627 | @Test public void testStandardsModeWithNullValuesInLoop () { 628 | test("first line\nsecond line", 629 | "first line\n{{#nullvalue}}foo\n{{/nullvalue}}\nsecond line", 630 | context("nullvalue", null)); 631 | } 632 | 633 | @Test public void testStandardsModeWithNullValuesInInverseLoop () { 634 | test("first line\nfoo \nsecond line", 635 | "first line\n{{^nullvalue}}foo{{/nullvalue}} \nsecond line", 636 | context("nullvalue", null)); 637 | } 638 | 639 | @Test public void testStandardsModeWithDotValue () { 640 | String tmpl = "{{#foo}}:{{.}}:{{/foo}}"; 641 | String result = Mustache.compiler().standardsMode(true).compile(tmpl). 642 | execute(Collections.singletonMap("foo", "bar")); 643 | check(":bar:", result); 644 | } 645 | 646 | @Test public void testStandardsModeWithNoParentContextSearching () { 647 | try { 648 | String tmpl = "{{#parent}}foo{{parentProperty}}bar{{/parent}}"; 649 | String result = Mustache.compiler().standardsMode(true).compile(tmpl). 650 | execute(context("parent", EMPTY, 651 | "parentProperty", "bar")); 652 | fail(); 653 | } catch (MustacheException me) {} // expected 654 | } 655 | 656 | @Test public void testMissingValue () { 657 | try { 658 | test("n/a", "{{missing}} {{notmissing}}", context("notmissing", "bar")); 659 | fail(); 660 | } catch (MustacheException me) {} // expected 661 | } 662 | 663 | @Test public void testMissingValueWithDefault () { 664 | test(Mustache.compiler().defaultValue(""), 665 | "bar", "{{missing}}{{notmissing}}", context("notmissing", "bar")); 666 | } 667 | 668 | @Test public void testMissingValueWithDefaultNonEmptyString () { 669 | test(Mustache.compiler().defaultValue("foo"), 670 | "foobar", "{{missing}}{{notmissing}}", context("notmissing", "bar")); 671 | } 672 | 673 | @Test public void testMissingValueWithDefaultSubstitution () { 674 | test(Mustache.compiler().defaultValue("?{{name}}?"), 675 | "?missing?bar", "{{missing}}{{notmissing}}", context("notmissing", "bar")); 676 | } 677 | 678 | @Test public void testMissingValueWithDefaultSubstitution2 () { 679 | test(Mustache.compiler().defaultValue("{{{{name}}}}"), 680 | "{{missing}}bar", "{{missing}}{{notmissing}}", context("notmissing", "bar")); 681 | } 682 | 683 | @Test public void testMissingValueWithDefaultSubstitution3 () { 684 | test(Mustache.compiler().defaultValue("{{?{{name}}?}}"), 685 | "{{?missing?}}bar", "{{missing}}{{notmissing}}", context("notmissing", "bar")); 686 | } 687 | 688 | @Test public void testNullValueGetsDefault () { 689 | test(Mustache.compiler().defaultValue("foo"), 690 | "foobar", "{{nullvar}}{{nonnullvar}}", context("nonnullvar", "bar", "nullvar", null)); 691 | } 692 | 693 | @Test public void testMissingValueWithNullDefault () { 694 | try { 695 | Object ctx = context("notmissing", "bar"); 696 | // no field or method for 'missing' 697 | test(Mustache.compiler().nullValue(""), "bar", "{{missing}}{{notmissing}}", ctx); 698 | fail(); 699 | } catch (MustacheException me) {} // expected 700 | } 701 | 702 | @Test public void testInvalidTripleMustache () { 703 | try { 704 | Mustache.compiler().compile("{{{foo}}"); 705 | fail("Expected MustacheParseException"); 706 | } catch (MustacheParseException e) { 707 | check("Invalid triple-mustache tag: {{{foo}} @ line 1", e.getMessage()); 708 | } 709 | try { 710 | Mustache.compiler().compile("{{{foo}}]"); 711 | fail("Expected MustacheParseException"); 712 | } catch (MustacheParseException e) { 713 | check("Invalid triple-mustache tag: {{{foo}}] @ line 1", e.getMessage()); 714 | } 715 | } 716 | 717 | @Test public void testNullValueGetsNullDefault () { 718 | test(Mustache.compiler().nullValue("foo"), 719 | "foobar", "{{nullvar}}{{nonnullvar}}", context("nonnullvar", "bar", "nullvar", null)); 720 | } 721 | 722 | @Test public void testCompilingDoesntChangeCompilersDelimiters() { 723 | Mustache.Compiler compiler = Mustache.compiler(); 724 | Object ctx = context("variable", "value"); 725 | test(compiler, "value", "{{=<% %>=}}<% variable %>", ctx); 726 | test(compiler, "value", "{{=<% %>=}}<% variable %>", ctx); 727 | } 728 | 729 | @Test public void testLambda1 () { 730 | test("Willy is awesome.", "{{#bold}}{{name}} is awesome.{{/bold}}", 731 | context("name", "Willy", "bold", new Mustache.Lambda() { 732 | public void execute (Template.Fragment frag, Writer out) throws IOException { 733 | out.write(""); 734 | frag.execute(out); 735 | out.write(""); 736 | } 737 | })); 738 | } 739 | 740 | @Test public void testLambda2 () { 741 | test("Slug bug potato!", "{{#l}}1{{/l}} {{#l}}2{{/l}} {{#l}}{{three}}{{/l}}", 742 | context("three", "3", "l", new Mustache.Lambda() { 743 | public void execute (Template.Fragment frag, Writer out) throws IOException { 744 | out.write(lookup(frag.execute())); 745 | } 746 | protected String lookup (String contents) { 747 | if (contents.equals("1")) return "Slug"; 748 | else if (contents.equals("2")) return "bug"; 749 | else if (contents.equals("3")) return "potato!"; 750 | else return "Who was that man?"; 751 | } 752 | })); 753 | } 754 | 755 | @Test public void testInvertibleLambda () { 756 | test("positive = positive, negative = negative, simple lambdas do still work", 757 | "{{#invertible}}positive{{/invertible}}, {{^invertible}}negative{{/invertible}}, " + 758 | "simple lambdas do {{^simple}}NOT {{/simple}}still work", 759 | context("invertible", new Mustache.InvertibleLambda() { 760 | public void execute (Template.Fragment frag, Writer out) throws IOException { 761 | out.write("positive = "); 762 | frag.execute(out); 763 | } 764 | public void executeInverse (Template.Fragment frag, Writer out) throws IOException { 765 | out.write("negative = "); 766 | frag.execute(out); 767 | } 768 | }, "simple", new Mustache.Lambda() { 769 | public void execute (Template.Fragment frag, Writer out) throws IOException { 770 | frag.execute(out); 771 | } 772 | })); 773 | } 774 | 775 | @Test public void testLambdaWithContext () { 776 | test("a in l1, a in l2", "{{#l1}}{{a}}{{/l1}}, {{#l2}}{{a}}{{/l2}}", context( 777 | "l1", new Mustache.Lambda() { 778 | public void execute (Template.Fragment frag, Writer out) throws IOException { 779 | frag.execute(context("a", "a in l1"), out); 780 | } 781 | }, 782 | "l2", new Mustache.Lambda() { 783 | public void execute (Template.Fragment frag, Writer out) throws IOException { 784 | frag.execute(context("a", "a in l2"), out); 785 | } 786 | })); 787 | } 788 | 789 | @Test public void testLambdaDecompile () { 790 | test("Foo {{a}}, Bar {{a}}", "{{#lam}}Foo {{a}}{{/lam}}, {{#lam}}Bar {{a}}{{/lam}}", 791 | context("lam", new Mustache.Lambda() { 792 | public void execute (Template.Fragment frag, Writer out) throws IOException { 793 | out.write(frag.decompile()); 794 | } 795 | })); 796 | // whitespace inside tags is dropped! 797 | test("Foo {{a}}, Bar {{a}}", "{{#lam}}Foo {{a}}{{/lam}}, {{#lam}}Bar {{ a }}{{/lam}}", 798 | context("lam", new Mustache.Lambda() { 799 | public void execute (Template.Fragment frag, Writer out) throws IOException { 800 | out.write(frag.decompile()); 801 | } 802 | })); 803 | // custom delimiters are ignored! 804 | test("Foo {{a}}, Bar {{a}}", 805 | "{{#lam}}Foo {{a}}{{/lam}}, {{=(( ))=}}((#lam))Bar ((a))((/lam))", 806 | context("lam", new Mustache.Lambda() { 807 | public void execute (Template.Fragment frag, Writer out) throws IOException { 808 | out.write(frag.decompile()); 809 | } 810 | })); 811 | // coalesced whitespace around section tags is preserved 812 | test("{{#section}}\n" + 813 | "{{a}}\n" + 814 | "{{/section}}", 815 | "{{#lam}}{{#section}}\n" + 816 | "{{a}}\n" + 817 | "{{/section}}{{/lam}}", 818 | context("lam", new Mustache.Lambda() { 819 | public void execute (Template.Fragment frag, Writer out) throws IOException { 820 | out.write(frag.decompile()); 821 | } 822 | })); 823 | } 824 | 825 | @Test public void testLambdaFragExecTemplate () { 826 | Template a = Mustache.compiler().compile("{{value}} should be A"); 827 | Template b = Mustache.compiler().compile("{{value}} should be B"); 828 | Mustache.Lambda lambda = new Mustache.Lambda() { 829 | public void execute (Template.Fragment frag, Writer out) { 830 | String which = frag.execute(); 831 | if (which.equals("A")) { 832 | frag.executeTemplate(a, out); 833 | } else if (which.equals("B")) { 834 | frag.executeTemplate(b, out); 835 | } else { 836 | throw new AssertionError("Invalid fragment content: " + which); 837 | } 838 | } 839 | }; 840 | String tmpl = "[{{#lam}}{{value}}{{/lam}}]"; 841 | test("[A should be A]", tmpl, context("lam", lambda, "value", "A")); 842 | test("[B should be B]", tmpl, context("lam", lambda, "value", "B")); 843 | } 844 | 845 | @Test public void testNonStandardDefaultDelims () { 846 | test(Mustache.compiler().withDelims("<% %>"), "bar", "<%foo%>", context("foo", "bar")); 847 | } 848 | 849 | protected String name; 850 | 851 | @Rule public TestRule watcher = new TestWatcher() { 852 | protected void starting(Description description) { 853 | name = description.getDisplayName(); 854 | } 855 | }; 856 | 857 | protected void test(Mustache.Compiler compiler, String expected, String template, Object ctx) { 858 | String actual = compiler.compile(template).execute(ctx); 859 | if (! Objects.equals(expected, actual)) { 860 | System.out.println(""); 861 | System.out.println("----------------------------------------"); 862 | System.out.println(""); 863 | System.out.println("Failed: " + name); 864 | System.out.println("Template : \"" + SpecTest.showWhitespace(template) + "\""); 865 | if (! partials.isEmpty()) { 866 | System.out.println("Partials : "); 867 | for (Entry e : partials.entrySet()) { 868 | System.out.println("\t" + e.getKey() + ": \"" + SpecTest.showWhitespace(e.getValue()) + "\""); 869 | } 870 | } 871 | System.out.println("Expected : \"" + SpecTest.showWhitespace(expected) + "\""); 872 | System.out.println("Result : \"" + SpecTest.showWhitespace(actual) + "\""); 873 | System.out.println("----------------------------------------"); 874 | System.out.println(""); 875 | } 876 | check(expected, actual); 877 | } 878 | 879 | protected void check (String expected, String output) { 880 | //assertEquals(uncrlf(expected), uncrlf(output)); 881 | assertEquals(SpecTest.showWhitespace(expected), SpecTest.showWhitespace(output)); 882 | 883 | } 884 | 885 | protected void test (String expected, String template, Object ctx) { 886 | test(Mustache.compiler(), expected, template, ctx); 887 | } 888 | 889 | protected static String uncrlf (String text) { 890 | return text == null ? null : text.replace("\r", "\\r").replace("\n", "\\n"); 891 | } 892 | 893 | protected static Object context (Object... data) { 894 | Map ctx = new HashMap(); 895 | for (int ii = 0; ii < data.length; ii += 2) { 896 | ctx.put(data[ii].toString(), data[ii+1]); 897 | } 898 | return ctx; 899 | } 900 | 901 | protected static Object EMPTY = context(); 902 | } 903 | --------------------------------------------------------------------------------