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