TemplateBuilder.java

package com.timtrense.template;

import com.timtrense.template.lang.std.StandardTemplateLanguage;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;

/**
 * The Builder Class for {@link Template templates}.
 * A {@link TemplateBuilder} has a {@link TemplateLanguage} that it uses to
 * parse template source texts.
 * The static default instance will use the {@link StandardTemplateLanguage} and
 * NOT fail on invalid placeholders.
 *
 * @author Tim Trense
 * @since 1.0
 */
@Data
@RequiredArgsConstructor
@AllArgsConstructor
/* See Template.@SuppressFBWarnings for explanation of circular dependency */
@SuppressFBWarnings("FCCD_FIND_CLASS_CIRCULAR_DEPENDENCY")
public class TemplateBuilder {

    /**
     * The default instance for building templates. Users may change the
     * contained {@link #templateLanguage} to their hearts contents globally
     * for the application while bootstrapping/configuring it.
     */
    public static final TemplateBuilder DEFAULT_INSTANCE = new TemplateBuilder();

    /**
     * The definition of a templates source text language used to parse those.
     */
    private @NonNull TemplateLanguage templateLanguage = new StandardTemplateLanguage();
    /**
     * Whether to abort the building of a template if a anticipated placeholder
     * could not be compiled by the {@link #templateLanguage}.
     * Defaults to {@code false} which means "keep the failed placeholder as a normal part of text"
     */
    private boolean failOnInvalidPlaceholder = false;

    /**
     * Uses the {@link StandardTemplateLanguage} to parse the given template
     * source text using {@link #buildTemplate(String)} on the {@link #DEFAULT_INSTANCE}.
     *
     * @param templateText the template source text
     * @return the parsed template, not null
     */
    public static Template build(@NonNull String templateText) {
        return DEFAULT_INSTANCE.buildTemplate(templateText);
    }

    /**
     * Uses the configured {@link #templateLanguage} to parse the given template
     * source text
     *
     * @param templateText the template source text
     * @return the parsed template, not null
     */
    public Template buildTemplate(@NonNull String templateText) {
        List<TemplatePart> parts = new LinkedList<>();
        Matcher placeholderMatcher = templateLanguage.getPlaceholderWrapperPattern().matcher(templateText);
        char escapeSequence = templateLanguage.getEscapeSequence();

        int searchPosition = 0;
        int templateTextLength = templateText.length();
        while (searchPosition < templateTextLength) {
            if (!placeholderMatcher.find(searchPosition)) {
                // just a trailing TextPart remaining
                break;
            }

            TemplatePart nextPart = templateLanguage.compilePlaceholder(placeholderMatcher);
            // nextPart == null : means could not compile

            if (nextPart == null && failOnInvalidPlaceholder) {
                int position = placeholderMatcher.start();
                throw new TemplateFormatException(
                        "Could not compile placeholder on template on \"" + templateText + "\" at " + position,
                        templateText, position);
            }

            int start = placeholderMatcher.start();

            if ((start > 0 && templateText.charAt(start - 1) == escapeSequence) || nextPart == null) {
                // add text part between last placeholder and escape sequence
                int endOfBetween = (nextPart != null) ? (start - 1) : start;
                parts.add(new TextPart(templateText.substring(searchPosition, endOfBetween)));

                // add text part containing the matched text
                searchPosition = placeholderMatcher.end();
                parts.add(new TextPart(templateText.substring(start, searchPosition)));
                continue;
            } else if (start > searchPosition) {
                // add text part between last placeholder and current placeholder
                parts.add(new TextPart(templateText.substring(searchPosition, start)));
            }

            parts.add(nextPart);

            searchPosition = placeholderMatcher.end();
        }

        if (searchPosition < templateTextLength) {
            // add remaining part
            parts.add(new TextPart(templateText.substring(searchPosition)));
        }

        return new Template(parts);
    }
}