001package io.jstach.opt.jmustache; 002 003import java.io.BufferedReader; 004import java.io.IOException; 005import java.io.InputStream; 006import java.io.InputStreamReader; 007import java.lang.System.Logger; 008import java.lang.System.Logger.Level; 009import java.nio.charset.StandardCharsets; 010import java.nio.file.Files; 011import java.nio.file.Path; 012import java.util.concurrent.atomic.AtomicBoolean; 013 014import org.eclipse.jdt.annotation.Nullable; 015import org.kohsuke.MetaInfServices; 016 017import com.samskivert.mustache.Mustache; 018import com.samskivert.mustache.Template; 019 020import io.jstach.jstache.JStacheLambda; 021import io.jstach.jstachio.JStachio; 022import io.jstach.jstachio.TemplateInfo; 023import io.jstach.jstachio.spi.JStachioConfig; 024import io.jstach.jstachio.spi.JStachioExtension; 025import io.jstach.jstachio.spi.Templates; 026 027/** 028 * Use JMustache instead of JStachio for rendering. The idea of this extension is to allow 029 * you to edit Mustache templates in real time without waiting for the compile reload 030 * cycle. 031 * <p> 032 * You are probably asking yourself <em>why do I need JMustache if I have JStachio</em>? 033 * Unfortunately JStachio needs the annotation processor to run <em>every time a template 034 * is changed!</em>. While there are incremental compilers like Eclipse that do support 035 * incrementally compiling annotations they are often not triggered via editing resources. 036 * Furthermore incremental compilation often just doesn't work. 037 * <p> 038 * Enter JMustache. Through reflection you can edit your templates while an application is 039 * running. Luckily <em>JMustache and JStachio are almost entirely compatible especially 040 * through this extension</em> which configures JMustache to act like JStachio. Even 041 * {@link JStacheLambda} will work. 042 * <p> 043 * <strong>The only major compatibility issue is that JMustache currently does not support 044 * mustache inheritance (parents and blocks)!</strong> 045 * <p> 046 * If this extension is enabled which it is by default if the ServiceLoader finds it 047 * JMustache will be used when a runtime filtered rendering call is made (see 048 * {@link io.jstach.jstachio.JStachio}). 049 * <p> 050 * How this works is this extension is a filter that checks to see if the statically 051 * generated renderer (template) can render and that its template is up-to-date. If it is 052 * not then JMustache will use the template meta data to construct its own template and 053 * then execute it. In some cases the annotation processor does not even have to run for 054 * this to work (see {@link Templates#getInfoByReflection(Class)}. 055 * <p> 056 * <strong>Strongly recommended you disable this in production via 057 * {@link #JSTACHIO_JMUSTACHE_DISABLE} or {@link #use}</strong> 058 * 059 * @author agentgt 060 * @see JStachio 061 */ 062@MetaInfServices(JStachioExtension.class) 063public class JMustacheRenderer extends AbstractJStacheEngine { 064 065 /** 066 * Property key of where jmustache will try to load template files. Default is 067 * <code>src/main/resources</code>. 068 */ 069 public static final String JSTACHIO_JMUSTACHE_SOURCE_PATH = "jstachio.jmustache.source"; 070 071 /** 072 * Property key to disable jmustache. Default is <code>false</code>. 073 */ 074 public static final String JSTACHIO_JMUSTACHE_DISABLE = "jstachio.jmustache.disable"; 075 076 private final AtomicBoolean use; 077 078 private volatile @Nullable String prefix = null; 079 080 private volatile @Nullable String suffix = null; 081 082 private String sourcePath = "src/main/resources"; 083 084 private Logger logger = JStachioConfig.noopLogger(); 085 086 private long initTime = System.currentTimeMillis(); 087 088 /** 089 * Enables JMustache 090 * @param flag true enables 091 * @return return this for builder like config 092 */ 093 public JMustacheRenderer use(boolean flag) { 094 use.set(flag); 095 log(flag); 096 return this; 097 } 098 099 /** 100 * A prefix to add to the output to know that JMustache is being used. 101 * @param prefix string to prefix output 102 * @return return this for builder like config 103 */ 104 public JMustacheRenderer prefix(@Nullable String prefix) { 105 this.prefix = prefix; 106 return this; 107 } 108 109 /** 110 * A suffix to append to the output to know that JMustache is being used. 111 * @param suffix string to suffix output 112 * @return return this for builder like config 113 */ 114 public JMustacheRenderer suffix(@Nullable String suffix) { 115 this.suffix = suffix; 116 return this; 117 } 118 119 /** 120 * Sets the relative to the project sourcePath for runtime lookup of templates. By 121 * default is <code>src/main/resources</code>. 122 * @param sourcePath by default is <code>src/main/resources</code> 123 * @return sourcePath should not be null 124 */ 125 public JMustacheRenderer sourcePath(String sourcePath) { 126 this.sourcePath = sourcePath; 127 return this; 128 } 129 130 protected void log(boolean flag) { 131 logger.log(Level.INFO, 132 "JMustache is now: " + (flag ? "enabled" : "disabled") + " using sourcePath: " + sourcePath); 133 } 134 135 protected void log(TemplateInfo template) { 136 if (logger.isLoggable(Level.DEBUG)) { 137 logger.log(Level.DEBUG, "Using JMustache. template: " + template.description()); 138 } 139 } 140 141 /** 142 * No-arg constructor for ServiceLoader 143 */ 144 public JMustacheRenderer() { 145 use = new AtomicBoolean(); 146 } 147 148 @Override 149 public void init(JStachioConfig config) { 150 logger = config.getLogger(getClass().getCanonicalName()); 151 sourcePath(config.requireProperty(JSTACHIO_JMUSTACHE_SOURCE_PATH, sourcePath)); 152 use(!config.getBoolean(JSTACHIO_JMUSTACHE_DISABLE)); 153 154 } 155 156 protected Mustache.Compiler createCompiler(TemplateInfo template) { 157 var compiler = Mustache.compiler() // 158 .standardsMode(false) // 159 .withEscaper(template.templateEscaper()::apply) // 160 .withFormatter(template.templateFormatter()::apply) // 161 .withCollector(new JStachioCollector()); 162 return compiler; 163 } 164 165 @Override 166 protected boolean execute(Object context, Appendable a, TemplateInfo template, boolean broken) throws IOException { 167 if (!use.get()) { 168 return false; 169 } 170 171 switch (template.templateSource()) { 172 case STRING -> { 173 Template t = createCompiler(template).compile(template.templateString()); 174 String results = t.execute(context); 175 if (prefix != null) { 176 a.append(prefix); 177 } 178 a.append(results); 179 return true; 180 } 181 case RESOURCE -> { 182 String templatePath = template.normalizePath(); 183 var path = Path.of(sourcePath, templatePath); 184 InputStream stream; 185 boolean _broken = template.lastLoaded() > 0 || broken; 186 if ((_broken && path.toFile().isFile()) || path.toFile().lastModified() > initTime) { 187 stream = openFile(path); 188 } 189 else if (broken) { 190 stream = openResource(templatePath); 191 } 192 else { 193 return false; 194 } 195 try (InputStream is = stream; 196 BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { 197 Template t = createCompiler(template).compile(br); 198 String results = t.execute(context); 199 a.append(results); 200 return true; 201 } 202 } 203 } 204 return false; 205 } 206 207 protected InputStream openFile(Path path) throws IOException { 208 InputStream is = Files.newInputStream(path); 209 if (logger.isLoggable(Level.INFO)) { 210 logger.log(Level.INFO, "Using JMustache. template:" + "file " + path); 211 } 212 return is; 213 } 214 215 protected InputStream openResource(String templatePath) throws IOException { 216 ClassLoader loader = Thread.currentThread().getContextClassLoader(); 217 InputStream is = loader.getResourceAsStream(templatePath); 218 if (is == null) { 219 throw new IOException("template not found. template: " + templatePath); 220 } 221 if (logger.isLoggable(Level.INFO)) { 222 logger.log(Level.INFO, "Using JMustache. template:" + "classpath " + templatePath); 223 } 224 return is; 225 } 226 227}