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}