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}