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

Reply via email to