001package io.jstach.jstachio;
002
003import java.io.IOException;
004import java.nio.charset.UnsupportedCharsetException;
005import java.util.NoSuchElementException;
006import java.util.Objects;
007import java.util.ServiceLoader;
008import java.util.function.Supplier;
009
010import io.jstach.jstache.JStache;
011import io.jstach.jstache.JStacheConfig;
012import io.jstach.jstachio.spi.AbstractJStachio;
013import io.jstach.jstachio.spi.JStachioConfig;
014import io.jstach.jstachio.spi.JStachioExtension;
015import io.jstach.jstachio.spi.JStachioFactory;
016
017/**
018 * Render models by using reflection or static catalog to lookup generated templates as
019 * well as apply filtering and fallback mechanisms.
020 * <h2>Example Usage</h2> <pre><code class="language-java">
021 * &#64;JStache(template = "Hello {{name}}!")
022 * public record HelloWorld(String name) {}
023 *
024 * public static String output(String name) {
025 *   //Normally you would have to use generated class HelloWorldRenderer
026 *   //but this JStachio allows you to render directly
027 *   //from the model.
028 *   return JStachio.render(new HelloWorld(name));
029 * }
030 * </code> </pre> Not only is the above more convenient than using the raw generated code
031 * it also allows additional custom runtime behavior like filtering as well as allows
032 * easier integration with web frameworks.
033 *
034 * <h2>Customize</h2>
035 *
036 * The default {@link JStachio} uses the {@link ServiceLoader} to load
037 * {@link JStachioExtension}s. You can customize it by adding jars that have provided
038 * {@link JStachioExtension}s or by {@linkplain JStachioConfig adjusting config}.
039 *
040 * <p>
041 *
042 * If you would like to create your own {@link JStachio} instead of the default you can
043 * either extend {@link AbstractJStachio} or use {@link JStachioFactory#builder()}. If you
044 * want your custom {@link JStachio} to be set as the default such that the static render
045 * methods on this class call it you can do that with {@link #setStatic(Supplier)}.
046 * <strong> While this interface is not sealed it is strongly recommended that you do not
047 * implement this interface! </strong> It has been left unsealed for mocking and testing
048 * purposes.
049 *
050 * @apiNote The static <strong><code>render</code></strong> methods are convenience
051 * methods that will by default use the ServiceLoader based JStachio which loads all
052 * extensions via the {@link ServiceLoader}.
053 * @see JStachioExtension
054 * @see JStache
055 * @see JStachioFactory#builder()
056 */
057public interface JStachio extends Renderer<Object> {
058
059        /**
060         * Finds a template by using the models class if possible and then applies filtering
061         * and then finally render the model by writing to the appendable.
062         * <p>
063         * {@inheritDoc}
064         */
065        @Override
066        default void execute(Object model, Appendable appendable) throws IOException {
067                Renderer.super.execute(model, appendable);
068        }
069
070        /**
071         * Finds a template by using the models class if possible and then applies filtering
072         * and then finally render the model by writing to the {@link StringBuilder}.
073         * <p>
074         * {@inheritDoc}
075         */
076        @Override
077        default StringBuilder execute(Object model, StringBuilder sb) {
078                return Renderer.super.execute(model, sb);
079        }
080
081        /**
082         * Finds a template by using the models class if possible and then applies filtering
083         * and then finally render the model to a String.
084         * <p>
085         * {@inheritDoc}
086         */
087        @Override
088        default String execute(Object model) {
089                return Renderer.super.execute(model);
090        }
091
092        /**
093         * Renders the passed in model directly to a binary stream possibly leveraging
094         * pre-encoded parts of the template. This <em>may</em> improve performance when
095         * rendering UTF-8 to an OutputStream as some of the encoding is done in advance.
096         * Because the encoding is done statically you cannot pass the charset in. The chosen
097         * charset comes from {@link JStacheConfig#charset()}.
098         * @param <A> output type
099         * @param <E> error type
100         * @param model a model assumed never to be <code>null</code>.
101         * @param output to write to.
102         * @return the passed in output for convenience
103         * @throws UnsupportedCharsetException if the encoding of the output does not match
104         * the template.
105         * @throws E if an error occurs while writing to output
106         */
107        public <A extends io.jstach.jstachio.Output.EncodedOutput<E>, E extends Exception> A write( //
108                        Object model, //
109                        A output) throws E;
110
111        /**
112         * Finds a template by model. This is useful if you need metadata before writing such
113         * as charset and media type for HTTP output which the template has.
114         * <p>
115         * The returned template is decorated if filtering is on and a filter that is not the
116         * template is applied.
117         * <p>
118         * Passing in a {@link TemplateModel} should work as well and the returned template
119         * will be able to execute the TemplateModel as though it were a regular model.
120         * @param model the actual model or a {@link TemplateModel} containing the model
121         * @return a filtered template
122         * @throws NoSuchElementException if a template is not found and no other lookup
123         * errors happen.
124         * @throws Exception if template cannot be found for unexpected reasons such as
125         * reflection errors.
126         * @apiNote implementations should handle {@link TemplateModel} passed in.
127         */
128        Template<Object> findTemplate(Object model) throws Exception;
129
130        /**
131         * Determines if this jstachio can render the model type (the class annotated by
132         * JStache).
133         * @param modelType the models class (<em>the one annotated with {@link JStache} and
134         * not the Templates class</em>)
135         * @return true if this jstachio can render instances of modelType
136         */
137        boolean supportsType(Class<?> modelType);
138
139        /**
140         * Executes the ServiceLoader instance of JStachio
141         * {@link #execute(Object, Appendable)}.
142         * @param model never <code>null</code>
143         * @param a appendable never <code>null</code>
144         * @throws IOException if there is an error using the appendable
145         * @see #execute(Object, Appendable)
146         */
147        public static void render(Object model, Appendable a) throws IOException {
148                of().execute(model, a);
149        }
150
151        /**
152         * Executes the ServiceLoader instance of JStachio
153         * {@link #execute(Object, StringBuilder)}.
154         * @param model never <code>null</code>
155         * @param a appendable never <code>null</code>
156         * @return the passed in {@link StringBuilder}
157         * @see #execute(Object, StringBuilder)
158         */
159        public static StringBuilder render(Object model, StringBuilder a) {
160                return of().execute(model, a);
161        }
162
163        /**
164         * Executes the ServiceLoader instance of JStachio {@link #execute(Object)}.
165         * @param model the root context model. Never <code>null</code>.
166         * @return the rendered string.
167         * @see #execute(Object)
168         */
169        public static String render(Object model) {
170                return of().execute(model);
171        }
172
173        /**
174         * Gets the static singleton jstachio.
175         * @return the jstachio from {@link #setStatic(Supplier)}
176         * @throws NullPointerException if jstachio is not found
177         * @see #setStatic(Supplier)
178         */
179        public static JStachio of() {
180                return JStachioHolder.get();
181        }
182
183        /**
184         * Gets default singleton ServiceLoader based jstachio.
185         * @return service loaded jstachio
186         */
187        public static JStachio defaults() {
188                return io.jstach.jstachio.spi.JStachioFactory.defaultJStachio();
189        }
190
191        /**
192         * Set the static singleton of JStachio.
193         * <p>
194         * Useful if you would like to avoid using the default ServiceLoader mechanism.
195         * @param jstachioProvider if null a NPE will be thrown.
196         * @apiNote the provider will be called on every call of {@link #of()} and thus to
197         * avoid constant recreation it is recommend the supplier be memoized/cached.
198         */
199        public static void setStatic(Supplier<JStachio> jstachioProvider) {
200                if (isNull(jstachioProvider)) {
201                        throw new NullPointerException("JStachio provider cannot be null");
202                }
203                JStachioHolder.provider = jstachioProvider;
204        }
205
206        private static boolean isNull(Object o) {
207                return o == null;
208        }
209
210}
211
212final class JStachioHolder {
213
214        static Supplier<JStachio> provider = JStachio::defaults;
215
216        private JStachioHolder() {
217        }
218
219        static JStachio get() {
220                return Objects.requireNonNull(provider.get(), "JStachio not found. This is probably a classloading issue.");
221        }
222
223}