All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.klojang.templates.Template Maven / Gradle / Ivy

The newest version!
package org.klojang.templates;

import org.klojang.check.Check;
import org.klojang.check.Tag;
import org.klojang.templates.x.ClassPathResolver;
import org.klojang.templates.x.FilePathResolver;
import org.klojang.templates.x.MTag;
import org.klojang.util.collection.IntArrayList;
import org.klojang.util.collection.IntList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.*;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toUnmodifiableList;
import static org.klojang.check.CommonChecks.*;
import static org.klojang.templates.TemplateLocation.STRING;
import static org.klojang.templates.x.Messages.ERR_NO_SUCH_TEMPLATE;
import static org.klojang.util.CollectionMethods.implode;

/**
 * The {@code Template} class is responsible for loading and parsing templates. It
 * also functions as a factory for {@link RenderSession} objects. {@code Template}
 * instances are unmodifiable, expensive-to-create and heavy-weight objects.
 * Generally though you should not cache them as this is already done by
 * Klojang Templates. You can disable template caching by means of a
 * system property. See {@link Setting#TMPL_CACHE_SIZE}. This can be useful during
 * development and/or debugging as the template file will be re-loaded and re-parsed
 * every time you press the refresh button in the browser, allowing you to edit the
 * template in between.
 *
 * @author Ayco Holleman
 */
public final class Template {

    @SuppressWarnings("unused")
    private static final Logger LOG = LoggerFactory.getLogger(Template.class);

    /**
     * The name given to the root template: "{root}". Any {@code Template} that is
     * explicitly instantiated by calling one of the {@code fromXXX()} methods gets
     * this name.
     */
    public static final String ROOT_TEMPLATE_NAME = "{root}";

    /**
     * Parses the specified string into a {@code Template}. If the string contains any
     * {@code include} tags (like {@code ~%%include:/path/to/foo.html%%}), the path
     * will be interpreted as a file system resource. Templates created from a string
     * are never cached.
     *
     * @param source the source code for the {@code Template}
     * @return a new {@code Template} instance
     * @throws ParseException if the template source contains a syntax error
     */
    public static Template fromString(String source) throws ParseException {
        Check.notNull(source, Tag.SOURCE);
        return new Parser(STRING, ROOT_TEMPLATE_NAME, source).parse();
    }

    /**
     * Parses the specified string into a {@code Template}. The specified class will be
     * used to include other templates using {@code Class.getResourceAsStream()}.
     * Templates created from a string are never cached.
     *
     * @param clazz  a {@code Class} object that provides access to the included
     *               template file by calling {@code getResourceAsStream} on it
     * @param source the source code for the {@code Template}
     * @return a {@code Template} instance
     * @throws ParseException if the template source contains a syntax error
     */
    public static Template fromString(Class clazz, String source)
            throws ParseException {
        Check.notNull(clazz, Tag.CLASS);
        Check.notNull(source, Tag.SOURCE);
        PathResolver resolver = new ClassPathResolver(clazz);
        TemplateLocation location = new TemplateLocation(resolver);
        return new Parser(location, ROOT_TEMPLATE_NAME, source).parse();
    }

    /**
     * Parses the specified resource into a {@code Template}. Templates created from a
     * classpath resource are always cached. Thus, calling this method multiple times
     * with the same {@code clazz} and {@code path} arguments will always return the
     * same instance. Make sure the provided class is publicly accessible.
     * Otherwise Klojang Templates cannot use it to open an {@code InputStream}
     * to the resource.
     *
     * @param clazz a {@code Class} object that provides access to the template
     *              file by calling {@code getResourceAsStream} on it
     * @param path  the location of the template file
     * @return a {@code Template} instance
     * @throws ParseException if the template source contains a syntax error
     */
    public static Template fromResource(Class clazz, String path)
            throws ParseException {
        Check.notNull(clazz, Tag.CLASS);
        Check.that(path).has(clazz::getResource, notNull(), "No such resource: ${arg}");
        PathResolver resolver = new ClassPathResolver(clazz);
        TemplateLocation location = new TemplateLocation(path, resolver);
        return TemplateCache.INSTANCE.get(location, ROOT_TEMPLATE_NAME);
    }

    /**
     * Parses the specified file into a {@code Template}. Templates created from file
     * are always cached. Thus, calling this method multiple times with the same
     * {@code path} argument will always return the same instance.
     *
     * @param path the path of the file to be parsed
     * @return a {@code Template} instance
     * @throws ParseException if the template source contains a syntax error
     */
    public static Template fromFile(String path) throws ParseException {
        Check.notNull(path).has(File::new, regularFile());
        TemplateLocation location = new TemplateLocation(path, new FilePathResolver());
        return TemplateCache.INSTANCE.get(location, ROOT_TEMPLATE_NAME);
    }

    /**
     * Creates a {@code Template} from the source provided by the specified
     * {@link PathResolver}. Templates created using a {@code PathResolver} are always
     * cached. Thus, calling this method multiple times with the same
     * {@code PathResolver} (as per its {@code equals()} method) and the same path will
     * always return the same instance.
     *
     * @param resolver the {@code PathResolver}
     * @param path     the path to be resolved by the {@code PathResolver}
     * @return a {@code Template} instance
     * @throws ParseException if the template source contains a syntax error
     */
    public static Template fromResolver(PathResolver resolver, String path)
            throws ParseException {
        Check.notNull(resolver, "resolver");
        Check.notNull(path, Tag.PATH);
        TemplateLocation location = new TemplateLocation(path, resolver);
        return TemplateCache.INSTANCE.get(location, ROOT_TEMPLATE_NAME);
    }

    private final String name;
    private final TemplateLocation location;
    private final List parts;
    private final Map varIndices;
    private final IntList textIndices;
    private final Map tmplIndices;
    // All variable names and nested template together
    private final List names;

    private Template parent;

    Template(String name, TemplateLocation location, List parts) {
        parts.forEach(p -> {
            p.setParentTemplate(this);
            if (p instanceof NestedTemplatePart ntp) {
                ntp.getTemplate().parent = this;
            }
        });
        this.name = name;
        this.location = location;
        this.parts = parts;
        this.varIndices = getVarIndices(parts);
        this.tmplIndices = getTmplIndices(parts);
        this.names = getNames(parts);
        this.textIndices = getTextIndices(parts);
    }

    Template(Template cached, String name) {
        this.name = name;
        this.location = cached.location;
        this.parts = cached.parts;
        this.varIndices = cached.varIndices;
        this.tmplIndices = cached.tmplIndices;
        this.nestedTemplates = cached.nestedTemplates;
        this.names = cached.names;
        this.textIndices = cached.textIndices;
    }

    /**
     * Returns the name of this {@code Template}. It this {@code Template} was
     * explicitly instantiated using one of the {@code fromXXX()} methods, its name
     * will be "{root}"; otherwise it is a nested template and its name will be
     * extracted from the source code (e.g. {@code ~%%begin:foo%}).
     *
     * @return the name of this {@code Template}
     */
    public String getName() {
        return name;
    }

    /**
     * Returns the template inside which this {@code Template} is nested. If this
     * {@code Template} is the root template (the template that was explicitly created
     * by a call to one of the {@code fromXXX()} methods), this method returns
     * {@code null}.
     *
     * @return the template inside which this {@code Template} is nested
     */
    public Template getParent() {
        return parent;
    }

    /**
     * Returns the root template of this (nested) {@code Template}. If this
     * {@code Template} is the root template, then this method returns
     * {@code this}. Only (and all) templates that were created using one of the
     * {@code fromXXX()} methods are root templates.
     *
     * @return the root template of this {@code Template}
     */
    public Template getRootTemplate() {
        if (parent == null) {
            return this;
        }
        Template t = parent;
        while (t.getParent() != null) {
            t = t.getParent();
        }
        return t;
    }

    /**
     * 

Returns an {@code Optional} containing the path to the source code of this * template, or an empty {@code Optional} if the template was * {@linkplain #fromString(String) created from a string}. In other words, for * {@code included} templates this method (by definition) returns a non-empty * {@code Optional}. For inline templates this method (by definition) returns an * empty {@code Optional}. For templates that were explicitly created using one of * the {@code fromXXX()} methods, the return value depends on whether it was * {@code fromString()} or one of the other {@code fromXXX()} methods. * * @return the path to the source code for this {@code Template} */ public Optional path() { return Optional.ofNullable(location.path()); } /** * Returns the names of the variables in this {@code Template}, in order of their * first appearance in the template. The returned set only contains the names of * variables that reside directly inside this {@code Template}. Variables * inside nested templates are ignored. The returned {@code Set} is unmodifiable. * * @return the names of all variables in this {@code Template} * @see TemplateUtils#getAllVariables(Template) * @see TemplateUtils#getAllVariableFQNames(Template) */ public Set getVariables() { return varIndices.keySet(); } /** * Returns all occurrences of all variables within this {@code Template}. Note that * a variable name may occur multiple times within the same template. * * @return all occurrences of all variables within this {@code Template} */ public List getVariableOccurrences() { return parts.stream() .filter(VariablePart.class::isInstance) .map(VariablePart.class::cast) .map(VariablePart::toOccurrence) .collect(toList()); } /** * Returns the total number of variables in this {@code Template}. Note that a * variable name may occur multiple times within the same template. This method * does not count the number of unique variable names. To get that number, * call {@link #getVariables() getVariables().size()}. * * @return the total number of variables in this {@code Template} */ public int countVariableOccurrences() { return (int) parts.stream().filter(VariablePart.class::isInstance).count(); } /** * Returns {@code true} if this {@code Template} directly contains a * variable with the specified name. Variables inside nested templates are not * considered. * * @param name the name of the variable * @return {@code true} if this {@code Template} contains a variable with the * specified name */ public boolean hasVariable(String name) { return Check.notNull(name).ok(varIndices::containsKey); } private List