I have replaced the old TemplateLoader interface with the new one in
the freemarker-3 branch, and "migrated" all the included
TemplateLoader-s and tests.
More eyes see more, so check it out if you can, and tell if you see
anything to improve.
As we don't have a DatabaseTemplateLoader yet, one of the important
points of this new TemplateLoader wasn't demonstrated now. A good way
of spotting the rough edges would be if someone implements that for
example.
Thanks!
Monday, February 6, 2017, 12:40:15 AM, Daniel Dekany wrote:
> In FM2 we had some problems with the TemplateLoader interface:
>
> - The major problem with is that to load a template you need to do
> multiple round trips to the storage mechanism: check if the template
> "file" exists, then get its last modification date, then read its
> content. This is particularly problematic for databases, but often
> even HTTP could pack these into a single round trip.
>
> - When the <#ftl encoding=...> header disagrees with the actual
> charset used for reading the template, the whole template has to
> be re-read (I/O!). That's because we get a Reader, so we can't
> rewind the InputStream behind it and start reading it again with
> the new charset.
>
> - To detect changes one can only use the last modification date (not a
> revision number or hash). It can be especially problematic if
> templates can change so fast, that the clock may doesn't tick
> between them.
>
> - TemplateLoader can't return meta-info like the output format (MIME
> type basically) of the template. Some storages could do that.
>
> - Some storages (like databases) support some kind of atomicity and
> transaction isolation, but the TemplateLoader mechanism doesn't allow
> you to utilize these.
>
> - The "template source" is a tricky to understand for implementators, as
> it servers both as a handle that can be closed, and as a long lived cache
> key component.
>
> I propose a totally new TemplateLoader design to counter all these
> problems in FM3. I tell the idea mostly in code bellow. Please share
> your thoughts! (I was also thinking about backporting this to FM2, but
> it was not feasible.)
>
>
> /**
> * This is the one that replaces the TemplateLoader of FM2.
> */
> public interface TemplateLoader {
>
> /**
> * Creates a new session, or returns {@code null} if the
> template loader implementation doesn't support sessions.
> * See {@link TemplateLoaderSession} for more information about sessions.
> */
> TemplateLoaderSession createSession();
>
> /**
> * Loads the template content together with meta-data such as
> the version (usually the last modification time),
> * optionally conditionally. Note how all these operations
> (existence check, up-to-date check, opening for reading)
> * were put into one atomic unit. This allows you utilize the
> capabilities of the storage mechanism to spare round
> * trips, or even to add atomicity guarantees.
> *
> * @param name
> * The name (template root directory relative path) of the
> template; same as in FM2.
> * @param ifSourceDiffersFrom
> * If we only want to load the template if its
> source differs from this. {@code null} if you want the
> * template to be loaded unconditionally. If this is {@code
> null} then the
> * {@code ifVersionDiffersFrom} parameter must be {@code null}
> too. See
> * {@link TemplateLoadingResult#getSource()} for more about
> versions.
> * @param ifVersionDiffersFrom
> * If we only want to load the template if its
> version (which is usually the last modification time)
> * differs from this. {@code null} if {@code
> ifSourceDiffersFrom} is {@code null}, or if the backing
> * storage from which the {@code
> ifSourceDiffersFrom} template source comes from doesn't store a version.
> * See {@link TemplateLoadingResult#getVersion()} for more
> about versions.
> *
> * @return Not {@code null}.
> */
> TemplateLoadingResult load(String name, TemplateLoadingSource
> ifSourceDiffersFrom, Serializable ifVersionDiffersFrom,
> TemplateLoaderSession session) throws IOException;
>
> /**
> * Invoked by {@link Configuration#clearTemplateCache()} to
> instruct this template loader to throw away its current
> * state (some kind of cache usually) and start afresh. For
> most {@link TemplateLoader} implementations this does
> * nothing.
> */
> void resetState();
>
> }
>
> /**
> * Return value of {@link TemplateLoader#load(String,
> TemplateLoadingSource, Serializable, TemplateLoaderSession)}
> */
> public final class TemplateLoadingResult {
>
> public static final TemplateLoadingResult NOT_FOUND = new
> TemplateLoadingResult(
> TemplateLoadingResultStatus.NOT_FOUND);
> public static final TemplateLoadingResult NOT_MODIFIED = new
> TemplateLoadingResult(
> TemplateLoadingResultStatus.NOT_MODIFIED);
>
> /**
> * Creates an instance with status {@link
> TemplateLoadingResultStatus#OPENED}, for a storage mechanism that
> * naturally returns the template content as sequence of {@code
> char}-s as opposed to a sequence of {@code byte}-s.
> * This is the case for example when you store the template in
> a database in a varchar or CLOB. Do <em>not</em> use
> * this constructor for stores that naturally return binary
> data instead (like files, class loader resources,
> * BLOB-s, etc.), because using this constructor will disable
> FreeMarker's charset selection mechanism.
> */
> public TemplateLoadingResult(TemplateLoadingSource source,
> Serializable version, Reader reader,
> TemplateConfiguration templateConfiguration) { ... }
>
> /**
> * Creates an instance with status {@link
> TemplateLoadingResultStatus#OPENED}, for a storage mechanism that
> * naturally returns the template content as sequence of {@code
> byte}-s as opposed to a sequence of {@code char}-s.
> * This is the case for example when you store the template in
> a file, classpath resource, or BLOB. Do <em>not</em>
> * use this constructor for stores that naturally return text
> instead (like database varchar and CLOB columns).
> */
> public TemplateLoadingResult(TemplateLoadingSource source,
> Serializable version, InputStream inputStream,
> TemplateConfiguration templateConfiguration) { ... }
>
> /**
> * Returns non-{@code null} exactly if {@link #getStatus()} is
> {@link TemplateLoadingResultStatus#OPENED} and the
> * backing store mechanism returns content as {@code byte}-s,
> as opposed to as {@code chars}-s. The return value is
> * always the same instance, no mater when and how many times this method
> is called.
> */
> public InputStream getInputStream() {
> return inputStream;
> }
>
> /**
> * Tells what kind of result this is; see the documentation of
> {@link TemplateLoadingResultStatus}.
> */
> public TemplateLoadingResultStatus getStatus() {
> return status;
> }
>
> /**
> * Same as "template source" FM2, but it's simpler, as it only
> focuses the usage as part of the cache key,
> * and can't be closed. If you aren't familiar with FM2
> template sources, see {@link TemplateLoadingSource}
> * below.
> */
> public TemplateLoadingSource getSource() {
> return source;
> }
>
> /**
> * This replaced the lastModifed of FM2, and is more flexible
> as it can be a revision number, a cryptographic hash,
> * etc. Only set if the result status is {@link
> TemplateLoadingResultStatus#OPENED} and the backing storage stores
> * such information. Version objects are compared with each
> other with their {@link Object#equals(Object)} method.
> */
> public Serializable getVersion() {
> return version;
> }
>
> /**
> * Similar to {@link #getInputStream()}, but used when the
> backing storage mechanism returns content as
> * {@code char}-s, as opposed to as {@code byte}-s.
> */
> public Reader getReader() {
> return reader;
> }
>
> /**
> * If {@link #getStatus()} is {@link
> TemplateLoadingResultStatus#OPENED}, and the template loader stores such
> * information (which is rare) then it returns the {@link
> TemplateConfiguration} applicable to the template,
> * otherwise it returns {@code null}. If there are {@link
> TemplateConfiguration}-s coming from other
> * sources, such as from {@link
> Configuration#getTemplateConfigurations()}, this won't replace them, but will
> be
> * merged with them, with properties coming from the returned
> {@link TemplateConfiguration} having the highest
> * priority.
> */
> public TemplateConfiguration getTemplateConfiguration() {
> return templateConfiguration;
> }
>
> }
>
> /**
> * Used for the value of {@link TemplateLoadingResult#getStatus()}.
> */
> public enum TemplateLoadingResultStatus {
>
> /**
> * The template with the requested name doesn't exist (not to
> be confused with "wasn't accessible due to error").
> */
> NOT_FOUND,
>
> /**
> * If the template was found, but its source and version is the
> same as that which was provided to
> * {@link TemplateLoader#load(String, TemplateLoadingSource,
> Serializable, TemplateLoaderSession)} (from a cache
> * presumably), so its content wasn't opened for reading.
> */
> NOT_MODIFIED,
>
> /**
> * If the template was found and its content is ready for reading.
> */
> OPENED
>
> }
>
> /**
> * The point of {@link TemplateLoadingSource} is that with their
> {@link #equals(Object)} method we can tell if two cache
> * entries were generated from the same physical resource or not.
> Comparing the template names isn't enough, because a
> * {@link TemplateLoader} may uses some kind of fallback mechanism,
> such as delegating to other {@link TemplateLoader}-s
> * until the template is found. Like if we have two {@link
> FileTemplateLoader}-s with different physical root
> * directories, both can contain {@code "foo/bar.ftl"}, but
> obviously the two files aren't the same.
> */
> public interface TemplateLoadingSource extends Serializable {
> // Empty
> }
>
> /**
> * Stores shared state between {@link TemplateLoader} operations
> that are executed close to each other in the same
> * thread. For example, a {@link TemplateLoader} that reads from a
> database might wants to store the database
> * connection in it for reuse. The goal of sessions is mostly to
> increase performance. However, because a
> * {@link TemplateCache#getTemplate(String, java.util.Locale,
> Object, String, boolean)} call is executed inside a single
> * session, sessions can be also be utilized to ensure that the
> template lookup (see {@link TemplateLookupStrategy})
> * happens on a consistent view (a snapshot) of the backing
> storage, if the backing storage mechanism supports such
> * thing.
> *
> * <p>
> * The {@link TemplateLoaderSession} implementation is (usually)
> specific to the {@link TemplateLoader}
> * implementation. If your {@link TemplateLoader} implementation
> can't take advantage of sessions, you don't have to
> * implement this interface, just return {@code null} for {@link
> TemplateLoader#createSession()}.
> *
> * <p>
> * {@link TemplateLoaderSession}-s should be lazy, that is,
> creating an instance should be very fast and should not
> * cause I/O. Only when (and if ever) the shared resource stored in
> the session is needed for the first time should the
> * shared resource be initialized.
> *
> * <p>
> * {@link TemplateLoaderSession}-s need not be thread safe.
> */
> public interface TemplateLoaderSession {
>
> /**
> * Closes this session, freeing any resources it holds. Further
> operations involving this session should fail, with
> * the exception of {@link #close()} itself, which should be silently
> ignored.
> */
> public void close() throws IOException;
>
> public boolean isClosed();
>
> }
>
>
> So, the template loading sequence for template not yet in the cache is
> something like this:
>
> session = templateLoader.createSession();
>
> // TemplateLookupStrategy kicks in... of course its some loop in reality:
> res = templateLoader.load("foo_en_US.ftl", null, null, session);
> if (res.status == NOT_FOUND) res =
> templateLoader.load("foo_en.ftl", null, null, session);
> if (res.status == NOT_FOUND) res = templateLoader.load("foo.ftl", null,
> null, session);
> if (res.status == NOT_FOUND) throw new TemplateNotFoundException();
>
> Reader reader;
> if (res.getReader() != null) {
> reader = res.getReader(); // Charset is not relevant
> } else {
> reader = new InputStreamReader(res.getInputStream,
> figureOutCharsetFromCfgAndSuch());
> }
>
> template = new Template(reader, ...);
> reader.close();
>
> session.close();
>
> For cached templates after the update delay you will fill those two
> null parameters with the source and version from the cache entry, an
> then you skip most of the rest if res.status == NOT_MODIFIED.
>
> I would also note that I have simplified the case of the
> InputStreamReader above. In reality you will have markSupported()
> true, have to add a mark at position 0, then drop the mark during
> parsing when you know that no <#ftl encoding=...> can come anymore, and
> so you are safe. If <#ftl encoding=...> interferes, we will have to
> reset() the stream, recreate the InputStreamReader, and parse again.
> (We don't re-read from the TemplateLoader, we just re-parse.)
>
--
Thanks,
Daniel Dekany