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

com.composum.sling.core.util.ValueEmbeddingReader Maven / Gradle / Ivy

There is a newer version: 4.3.4
Show newest version
package com.composum.sling.core.util;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.Formatter;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

/**
 * replace all '${key}' elements in the stream by their values from the value map;
 * escape '$' and '\' with a prepended '\'
 * - a key '${resource:/a/path/to/resource} is replaced by the content of the resource loaded by the class loader
 * - if a value is a Reader the content of the reader is copied as value; caution a reader object can be read only once
 * - a key can contain a format string (${name;format}); in this case the embedded value is formatted (@see #java.util.Formatter)
 */
public class ValueEmbeddingReader extends Reader {

    private static final Logger LOG = LoggerFactory.getLogger(ValueEmbeddingReader.class);

    public static final String TYPE_RESOURCE = "resource";

    public static final char PATH_SEPARATOR = '/';
    public static final char MEMBER_SEPARATOR = '.';
    public static final char TYPE_SEPARATOR = ':';
    public static final char FORMAT_SEPARATOR = ';';

    public static final int BUFSIZE = 1024;

    public static class Key {

        public String type = null;
        public final String name;
        public final String format;
        public final String pattern;

        public Key(String key) {
            pattern = key;
            int typeSep = key.indexOf(TYPE_SEPARATOR);
            if (typeSep > 0) {
                int pathSep = key.indexOf(PATH_SEPARATOR);
                if (pathSep < 0 || pathSep > typeSep) {
                    int memberSep = key.indexOf(MEMBER_SEPARATOR);
                    if (memberSep < 0 || memberSep > typeSep) {
                        type = key.substring(0, typeSep);
                        key = key.substring(typeSep + 1);
                    }
                }
            }
            int formatSep = key.indexOf(FORMAT_SEPARATOR);
            if (formatSep > 0) {
                name = key.substring(0, formatSep);
                format = key.substring(formatSep + 1);
            } else {
                name = key;
                format = null;
            }
        }

        @Override
        public String toString() {
            return pattern;
        }
    }

    protected final Reader reader;
    protected final ValueMap values;
    protected final Locale locale;
    protected final Class resourceContext;
    protected final ResourceBundle resourceBundle;
    protected boolean keepUnresolvable = false;

    protected boolean eof = false;
    protected char[] buf = new char[BUFSIZE * 2]; // reserve place for values (max length:  BUFSIZE)
    protected int off = 0;
    protected int len = 0;

    private transient Reader embed;

    /**
     * @param reader the text to read - probably with embedded value placeholders
     * @param values the set of available placeholders
     */
    public ValueEmbeddingReader(@NotNull Reader reader, @NotNull Map values) {
        this(reader, values, null, null, null);
    }

    /**
     * @param reader          the text to read - probably with embedded value placeholders
     * @param values          the set of available placeholders
     * @param locale          the locale to use for value formatting
     * @param resourceContext the context class for resource loading
     */
    public ValueEmbeddingReader(@NotNull Reader reader, @NotNull Map values,
                                @Nullable Locale locale, @Nullable Class resourceContext) {
        this(reader, values, locale, resourceContext, null);
    }

    /**
     * @param reader          the text to read - probably with embedded value placeholders
     * @param values          the set of available placeholders
     * @param locale          the locale to use for value formatting
     * @param resourceContext the context class for resource loading
     * @param resourceBundle  the translations bundle (switches translation on)
     */
    public ValueEmbeddingReader(@NotNull Reader reader, @NotNull Map values,
                                @Nullable Locale locale, @Nullable Class resourceContext,
                                @Nullable ResourceBundle resourceBundle) {
        this.reader = reader;
        this.values = values instanceof ValueMap ? ((ValueMap) values) : new ValueMapDecorator(values);
        this.locale = locale != null ? locale : Locale.getDefault();
        this.resourceContext = resourceContext != null ? resourceContext : values.getClass();
        this.resourceBundle = resourceBundle;
    }

    public boolean isKeepUnresolvable(final boolean... decision) {
        return decision.length > 0 ? (keepUnresolvable = decision[0]) : keepUnresolvable;
    }

    protected void embedKey(Key key) {
        embed = new StringReader(keepUnresolvable ? ("${" + key + "}") : "");
    }

    @Override
    public void close() throws IOException {
        reader.close();
    }

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
        if (embed != null) {
            if (this.len > 0) { // flush buffer before embedding a reader
                return copy(cbuf, off, len);
            }
            int count = embed.read(cbuf, off, len); // embed a readers content
            if (count >= 0) {
                return count;
            }
            embed.close(); // stop embedding on readers EOF
            embed = null;
        }
        if (this.len < len && !eof) {
            load();
        }
        if (this.len < 1) {
            return -1;
        }
        return copy(cbuf, off, len);
    }

    protected int copy(char[] cbuf, int off, int len) {
        int count = Math.min(this.len, len);
        if (count > 0) {
            System.arraycopy(this.buf, this.off, cbuf, off, count);
            this.off += count;
            this.len -= count;
        }
        return count;
    }

    protected void load() throws IOException {
        if (off > 0) {
            System.arraycopy(buf, off, buf, 0, len);
            off = 0;
        }
        while (!eof && len < BUFSIZE) {
            int token = reader.read();
            if (token < 0) {
                eof = true;
            } else if (token == '\\') { // escaped '$' or '\'?
                int next = reader.read();
                if (next < 0) {
                    buf[len++] = '\\';
                    eof = true;
                } else {
                    if (next != '\\' && next != '$') {
                        buf[len++] = '\\';
                    }
                    buf[len++] = (char) next;
                }
            } else if (token == '$') { // '${...} ?
                int next = reader.read();
                if (next < 0) {
                    buf[len++] = '$';
                    eof = true;
                } else {
                    if (next == '{') {
                        StringBuilder keyBuffer = new StringBuilder();
                        while (!eof && (next = reader.read()) != '}') {
                            if (next < 0) {
                                eof = true;
                            } else {
                                keyBuffer.append((char) next);
                            }
                        }
                        if (!eof) {
                            Key key = new Key(keyBuffer.toString().trim());
                            if (TYPE_RESOURCE.equals(key.type)) {
                                if (resourceContext != null) {
                                    InputStream stream = resourceContext.getResourceAsStream(key.name);
                                    if (stream != null) {
                                        // recursive embedding of a resource file with the given value set
                                        embed = new ValueEmbeddingReader(
                                                new InputStreamReader(stream, StandardCharsets.UTF_8),
                                                values, locale, resourceContext, resourceBundle);
                                        return; // stop buffering up to the end of the embedded reader
                                    } else {
                                        LOG.warn("resource '{}' not available; probably invalid context ({})", key.name, resourceContext.getName());
                                    }
                                }
                                embedKey(key);
                                return; // stop buffering up to the end of the embedded reader
                            } else {
                                Object value = values.get(key.name);
                                if (value == null && resourceBundle != null) {
                                    try {
                                        value = resourceBundle.getString(key.name);
                                    } catch (MissingResourceException mrex) {
                                        value = key.name;
                                    }
                                }
                                if (value instanceof Reader) {
                                    // recursive embedding of a reader object with the given value set
                                    embed = new ValueEmbeddingReader((Reader) value, values,
                                            locale, resourceContext, resourceBundle);
                                    return; // stop buffering up to the end of the embedded reader
                                } else if (value != null) {
                                    String string;
                                    if (StringUtils.isNotBlank(key.format)) {
                                        StringBuilder builder = new StringBuilder();
                                        Formatter formatter = new Formatter(builder);
                                        formatter.format(locale, key.format, value);
                                        string = builder.toString();
                                    } else {
                                        string = value.toString();
                                    }
                                    string.getChars(0, string.length(), buf, len);
                                    len += string.length();
                                } else {
                                    embedKey(key);
                                    return; // stop buffering up to the end of the embedded reader
                                }
                            }
                        } else {
                            buf[len++] = '$';
                            buf[len++] = '{';
                            keyBuffer.getChars(0, keyBuffer.length(), buf, len);
                            len += keyBuffer.length();
                        }
                    } else {
                        buf[len++] = '$';
                        buf[len++] = (char) next;
                    }
                }
            } else {
                buf[len++] = (char) token;
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy