public static Templator parseTemplate(final String template,
                                          final Function<String, String> paramFormatter,
                                          final Function<String, String> staticTextFormatter) {
        if (NullSafe.isEmptyString(template)) {
            return Templator.EMPTY_TEMPLATE;
        } else {
            final List<PartExtractor> funcList = new ArrayList<>();
            final StringBuilder sb = new StringBuilder();
            char lastChar = 0;
            boolean inVariable = false;
            final Function<String, String> effectiveParamFormatter = paramFormatter != null
                    ? str -> paramFormatter.apply(NullSafe.string(str))
                    : NullSafe::string;

            for (final char chr : template.toCharArray()) {
                if (chr == '{' && lastChar == '$') {
                    inVariable = true;
                    if (!sb.isEmpty()) {
                        // Stuff before must be static text
                        final String staticText = format(sb.toString(), staticTextFormatter);
                        funcList.add(StaticPart.of(staticText));
                        LOGGER.debug("Adding static text func for '{}'", staticText);
                        sb.setLength(0);
                    }
                } else if (inVariable && chr == '}') {
                    inVariable = false;
                    final String key = sb.toString();
                    funcList.add(attributeMap ->
                            NullSafe.hasEntries(attributeMap)
                                    ? effectiveParamFormatter.apply(attributeMap.get(key))
                                    : "");
                    LOGGER.debug("Adding header attributeMap value func for key '{}'", key);
                    sb.setLength(0);
                } else if (chr != '$') {
                    // might be static text or the name of the key
                    sb.append(chr);
                }
                lastChar = chr;
            }
            if (inVariable) {
                throw new IllegalArgumentException(LogUtil.message(
                        "Unclosed variable in template '{}'", template));
            }

            // Pick up any trailing static text
            if (!sb.isEmpty()) {
                // Stuff before must be static text
                final String staticText = format(sb.toString(), staticTextFormatter);
                funcList.add(StaticPart.of(staticText));
                sb.setLength(0);
            }
            return new Templator(funcList);
        }
    }
Parses a template like '${accountId}_${component}_static_text' and returns
a {@link Templator} that can be used many times to build strings from
the parsed template.
<p>
The case sensitivity will depend on the implementation of the map
passed to {@link Templator#parseTemplate(String)}.
</p>

@param template            The template string to parse.
@param paramFormatter      An optional formatter that will be called on all
                           parameterised parts of the resolved string,
                           e.g. {@link String#toUpperCase()} and/or to replace unwanted chars.
@param staticTextFormatter An optional formatter that will be called on all
                           static parts of the template
                           e.g. {@link String#toUpperCase()} and/or to replace unwanted chars.
stroom-util/src/main/java/stroom/util/string/TemplateUtil.java