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

io.bdeploy.common.util.TemplateHelper Maven / Gradle / Ivy

Go to download

Public API including dependencies, ready to be used for integrations and plugins.

There is a newer version: 7.3.6
Show newest version
package io.bdeploy.common.util;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TemplateHelper {

    private static final Logger log = LoggerFactory.getLogger(TemplateHelper.class);

    private static final String PATTERN_START = "{{";
    private static final String PATTERN_END = "}}";
    private static final Pattern PATTERN = Pattern.compile(Pattern.quote(PATTERN_START) + "(.*?)" + Pattern.quote(PATTERN_END));

    private TemplateHelper() {
    }

    /**
     * Resolves all variable references in the given list of strings using the given resolver.
     *
     * @param values
     *            the raw values, potentially containing variable references.
     * @param valueResolver
     *            knows how to replace references with the actual content
     * @return the resolved strings
     */
    public static List process(List values, VariableResolver valueResolver) {
        return process(values, valueResolver, v -> true);
    }

    /**
     * Asks the given resolver callback whether or not to resolve the variables contained in the given list of strings. The
     * callback can be used to skip resolving of given variables.
     *
     * @param values
     *            the raw values, potentially containing variable references.
     * @param valueResolver
     *            knows how to replace references with the actual content
     * @param shouldResolve
     *            callback to ask whether or not to resolve the given variable
     * @return the resolved strings
     */
    public static List process(List values, VariableResolver valueResolver, ShouldResolve shouldResolve) {
        return values.stream().map(v -> process(v, valueResolver, shouldResolve)).collect(Collectors.toList());
    }

    /**
     * Resolves all variable references in the given string using the given resolver.
     *
     * @param value
     *            the raw value, potentially containing variable references.
     * @param valueResolver
     *            knows how to replace references with the actual content
     * @return the resolved string
     */
    public static String process(String value, VariableResolver valueResolver) {
        return process(value, valueResolver, v -> true);
    }

    /**
     * Resolves all variable references in the given String, using the given resolver. The
     * callback can be used to skip resolving of given variables.
     *
     * @param value
     *            the raw value, potentially containing variable references.
     * @param valueResolver
     *            knows how to replace references with the actual content
     * @param shouldResolve
     *            callback to ask whether or not to resolve the given variable
     * @return the resolved string
     */
    public static String process(String value, VariableResolver valueResolver, ShouldResolve shouldResolve) {
        if (value == null || !value.contains(PATTERN_START)) {
            return value;
        }
        return doProcess(value, valueResolver, shouldResolve, null, new ArrayDeque<>());
    }

    /**
     * Resolves all variable references in the given String, using the given resolver. The
     * callback can be used to skip resolving of given variables.
     *
     * @param value
     *            the raw value, potentially containing variable references.
     * @param valueResolver
     *            knows how to replace references with the actual content
     * @param shouldResolve
     *            callback to ask whether or not to resolve the given variable
     * @param valueId
     *            the id that denotes the origin of the value being processed (e.g. config filename, process id).
     *            Will be mentioned in exception message.
     *            If valueId is null, value will be passed into exception message instead
     * @return the resolved string
     */
    public static String process(String value, VariableResolver valueResolver, ShouldResolve shouldResolve, String valueId) {
        if (value == null || !value.contains(PATTERN_START)) {
            return value;
        }
        return doProcess(value, valueResolver, shouldResolve, valueId, new ArrayDeque<>());
    }

    /**
     * Recursively resolves the given input.
     */
    private static String doProcess(String value, VariableResolver valueResolver, ShouldResolve shouldResolve, String valueId,
            Deque cycleDetector) {
        if (cycleDetector.contains(value)) {
            cycleDetector.push(value);
            String chain = String.join(" -> ", cycleDetector);
            log.error("Infinite loop detected: {}", chain);
            throw new IllegalArgumentException("Infinite loop detected: " + chain);
        }

        StringBuilder builder = new StringBuilder();
        int currentStart = 0;

        Matcher m = PATTERN.matcher(value);
        while (m.find()) {
            // Resolve next variable if desired
            String nextMatch = m.group(1);
            String resolved = nextMatch;
            if (shouldResolve != null && shouldResolve.apply(nextMatch)) {
                resolved = valueResolver.apply(nextMatch);
                if (resolved == null) {
                    throw new IllegalArgumentException("Cannot find replacement for variable " + nextMatch + " while processing "
                            + (valueId == null ? value : valueId));
                }
                // Resolve recursive as the replacement can also contains templates
                cycleDetector.push(value);
                resolved = doProcess(resolved, valueResolver, shouldResolve, valueId, cycleDetector);
                cycleDetector.pop();
            } else {
                // Keep pattern for the unresolved intact so that we can resolve it later
                resolved = PATTERN_START + resolved + PATTERN_END;
            }

            // append string from beginning or previous end to start of variable match
            // not using appendReplacement and appendTail since they are slow.
            builder.append(value.substring(currentStart, m.start()));
            builder.append(resolved);

            // Update indices for next round
            currentStart = m.end();
        }

        // Append remaining content of the input
        builder.append(value.substring(currentStart, value.length()));
        return builder.toString();
    }

    /**
     * Processes each template reference in the given input by calling the given processor.
     * 

* Whatever result is returned by the processor will be re-inserted *as template variable* (including * the pattern start and end markers!) in the string. */ public static String updateReferences(String value, UnaryOperator processor) { StringBuilder builder = new StringBuilder(); int currentStart = 0; Matcher m = PATTERN.matcher(value); while (m.find()) { // Resolve next variable if desired String nextMatch = m.group(1); String resolved = nextMatch; // Keep pattern for whatever the processor returned resolved = PATTERN_START + processor.apply(resolved) + PATTERN_END; // append string from beginning or previous end to start of variable match // not using appendReplacement and appendTail since they are slow. builder.append(value.substring(currentStart, m.start())); builder.append(resolved); // Update indices for next round currentStart = m.end(); } // Append remaining content of the input builder.append(value.substring(currentStart, value.length())); return builder.toString(); } /** * @param path a folder containing files to recursively read/expand templates/write. * @param resolver the {@link VariableResolver} used to resolve all references. */ public static void processFileTemplates(Path path, VariableResolver resolver) { if (!Files.isDirectory(path)) { return; // nothing to do. } try (Stream allPaths = Files.walk(path)) { allPaths.filter(Files::isRegularFile).forEach(p -> processFileTemplate(p, resolver)); } catch (IOException e) { log.error("Cannot walk configuration file tree", e); } } private static void processFileTemplate(Path file, VariableResolver resolver) { try (InputStream check = Files.newInputStream(file)) { if (!StreamHelper.isTextFile(check)) { return; } } catch (IOException e) { throw new IllegalStateException("Cannot check if file is a text file: " + file, e); } try { String content = new String(Files.readAllBytes(file), StandardCharsets.UTF_8); String processed = TemplateHelper.process(content, resolver); Files.write(file, processed.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC); } catch (Exception e) { // might have missing variable references, since we only 'see' what is on our // node. Applications from other nodes are not available. log.warn("Cannot process configuration file: {}: {}", file, e.toString()); if (log.isDebugEnabled()) { log.debug("Error details", e); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy