    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.text.io;
    019
    020import java.io.FilterReader;
    021import java.io.IOException;
    022import java.io.Reader;
    023import java.util.Objects;
    024
    025import org.apache.commons.text.StringSubstitutor;
    026import org.apache.commons.text.TextStringBuilder;
    027import org.apache.commons.text.matcher.StringMatcher;
    028import org.apache.commons.text.matcher.StringMatcherFactory;
    029
    030/**
    031 * A {@link Reader} that performs string substitution on a source {@code Reader} using a {@link StringSubstitutor}.
    032 *
    033 * <p>
    034 * Using this Reader avoids reading a whole file into memory as a {@code String} to perform string substitution, for
    035 * example, when a Servlet filters a file to a client.
    036 * </p>
    037 * <p>
    038 * This class is not thread-safe.
    039 * </p>
    040 *
    041 * @since 1.9
    042 */
    043public class StringSubstitutorReader extends FilterReader {
    044
    045    /** The end-of-stream character marker. */
    046    private static final int EOS = -1;
    047
    048    /** Our internal buffer. */
    049    private final TextStringBuilder buffer = new TextStringBuilder();
    050
    051    /** End-of-Stream flag. */
    052    private boolean eos;
    053
    054    /** Matches escaped variable starts. */
    055    private final StringMatcher prefixEscapeMatcher;
    056
    057    /** Internal buffer for {@link #read()} method. */
    058    private final char[] read1CharBuffer = {0};
    059
    060    /** The underlying StringSubstitutor. */
    061    private final StringSubstitutor stringSubstitutor;
    062
    063    /** We don't always want to drain the whole buffer. */
    064    private int toDrain;
    065
    066    /**
    067     * Constructs a new instance.
    068     *
    069     * @param reader the underlying reader containing the template text known to the given {@code StringSubstitutor}.
    070     * @param stringSubstitutor How to replace as we read.
    071     * @throws NullPointerException if {@code reader} is {@code null}.
    072     * @throws NullPointerException if {@code stringSubstitutor} is {@code null}.
    073     */
    074    public StringSubstitutorReader(final Reader reader, final StringSubstitutor stringSubstitutor) {
    075        super(reader);
    076        this.stringSubstitutor = Objects.requireNonNull(stringSubstitutor);
    077        this.prefixEscapeMatcher = StringMatcherFactory.INSTANCE.charMatcher(stringSubstitutor.getEscapeChar())
    078            .andThen(stringSubstitutor.getVariablePrefixMatcher());
    079    }
    080
    081    /**
    082     * Buffers the requested number of characters if available.
    083     */
    084    private int buffer(final int requestReadCount) throws IOException {
    085        final int actualReadCount = buffer.readFrom(super.in, requestReadCount);
    086        eos = actualReadCount == EOS;
    087        return actualReadCount;
    088    }
    089
    090    /**
    091     * Reads a requested number of chars from the underlying reader into the buffer. On EOS, set the state is DRAINING,
    092     * drain, and return a drain count, otherwise, returns the actual read count.
    093     */
    094    private int bufferOrDrainOnEos(final int requestReadCount, final char[] target, final int targetIndex,
    095        final int targetLength) throws IOException {
    096        final int actualReadCount = buffer(requestReadCount);
    097        return drainOnEos(actualReadCount, target, targetIndex, targetLength);
    098    }
    099
    100    /**
    101     * Drains characters from our buffer to the given {@code target}.
    102     */
    103    private int drain(final char[] target, final int targetIndex, final int targetLength) {
    104        final int actualLen = Math.min(buffer.length(), targetLength);
    105        final int drainCount = buffer.drainChars(0, actualLen, target, targetIndex);
    106        toDrain -= drainCount;
    107        if (buffer.isEmpty() || toDrain == 0) {
    108            // nothing or everything drained.
    109            toDrain = 0;
    110        }
    111        return drainCount;
    112    }
    113
    114    /**
    115     * Drains from the buffer to the target only if we are at EOS per the input count. If input count is EOS, drain and
    116     * returns the drain count, otherwise return the input count. If draining, the state is set to DRAINING.
    117     */
    118    private int drainOnEos(final int readCountOrEos, final char[] target, final int targetIndex,
    119        final int targetLength) {
    120        if (readCountOrEos == EOS) {
    121            // At EOS, drain.
    122            if (buffer.isNotEmpty()) {
    123                toDrain = buffer.size();
    124                return drain(target, targetIndex, targetLength);
    125            }
    126            return EOS;
    127        }
    128        return readCountOrEos;
    129    }
    130
    131    /**
    132     * Tests if our buffer matches the given string matcher at the given position in the buffer.
    133     */
    134    private boolean isBufferMatchAt(final StringMatcher stringMatcher, final int pos) {
    135        return stringMatcher.isMatch(buffer, pos) == stringMatcher.size();
    136    }
    137
    138    /**
    139     * Tests if we are draining.
    140     */
    141    private boolean isDraining() {
    142        return toDrain > 0;
    143    }
    144
    145    /**
    146     * Reads a single character.
    147     *
    148     * @return a character as an {@code int} or {@code -1} for end-of-stream.
    149     * @throws IOException If an I/O error occurs
    150     */
    151    @Override
    152    public int read() throws IOException {
    153        int count = 0;
    154        // ask until we get a char or EOS
    155        do {
    156            count = read(read1CharBuffer, 0, 1);
    157            if (count == EOS) {
    158                return EOS;
    159            }
    160            // keep on buffering
    161        } while (count < 1);
    162        return read1CharBuffer[0];
    163    }
    164
    165    /**
    166     * Reads characters into a portion of an array.
    167     *
    168     * @param target Target buffer.
    169     * @param targetIndexIn Index in the target at which to start storing characters.
    170     * @param targetLengthIn Maximum number of characters to read.
    171     *
    172     * @return The number of characters read, or -1 on end of stream.
    173     * @throws IOException If an I/O error occurs
    174     */
    175    @Override
    176    public int read(final char[] target, final int targetIndexIn, final int targetLengthIn) throws IOException {
    177        // The whole thing is inefficient because we must look for a balanced suffix to match the starting prefix
    178        // Trying to substitute an incomplete expression can perform replacements when it should not.
    179        // At a high level:
    180        // - if draining, drain until empty or target length hit
    181        // - copy to target until we find a variable start
    182        // - buffer until a balanced suffix is read, then substitute.
    183        if (eos && buffer.isEmpty()) {
    184            return EOS;
    185        }
    186        if (targetLengthIn <= 0) {
    187            // short-circuit: ask nothing, give nothing
    188            return 0;
    189        }
    190        // drain check
    191        int targetIndex = targetIndexIn;
    192        int targetLength = targetLengthIn;
    193        if (isDraining()) {
    194            // drain as much as possible
    195            final int drainCount = drain(target, targetIndex, Math.min(toDrain, targetLength));
    196            if (drainCount == targetLength) {
    197                // drained length requested, target is full, can only do more in the next invocation
    198                return targetLength;
    199            }
    200            // drained less than requested, target not full.
    201            targetIndex += drainCount;
    202            targetLength -= drainCount;
    203        }
    204        // BUFFER from the underlying reader
    205        final int minReadLenPrefix = prefixEscapeMatcher.size();
    206        // READ enough to test for an [optionally escaped] variable start
    207        int readCount = buffer(readCount(minReadLenPrefix, 0));
    208        if (buffer.length() < minReadLenPrefix && targetLength < minReadLenPrefix) {
    209            // read less than minReadLenPrefix, no variable possible
    210            final int drainCount = drain(target, targetIndex, targetLength);
    211            targetIndex += drainCount;
    212            final int targetSize = targetIndex - targetIndexIn;
    213            return eos && targetSize <= 0 ? EOS : targetSize;
    214        }
    215        if (eos) {
    216            // EOS
    217            stringSubstitutor.replaceIn(buffer);
    218            toDrain = buffer.size();
    219            final int drainCount = drain(target, targetIndex, targetLength);
    220            targetIndex += drainCount;
    221            final int targetSize = targetIndex - targetIndexIn;
    222            return eos && targetSize <= 0 ? EOS : targetSize;
    223        }
    224        // PREFIX
    225        // buffer and drain until we find a variable start, escaped or plain.
    226        int balance = 0;
    227        final StringMatcher prefixMatcher = stringSubstitutor.getVariablePrefixMatcher();
    228        int pos = 0;
    229        while (targetLength > 0) {
    230            if (isBufferMatchAt(prefixMatcher, 0)) {
    231                balance = 1;
    232                pos = prefixMatcher.size();
    233                break;
    234            } else if (isBufferMatchAt(prefixEscapeMatcher, 0)) {
    235                balance = 1;
    236                pos = prefixEscapeMatcher.size();
    237                break;
    238            }
    239            // drain first char
    240            final int drainCount = drain(target, targetIndex, 1);
    241            targetIndex += drainCount;
    242            targetLength -= drainCount;
    243            if (buffer.size() < minReadLenPrefix) {
    244                readCount = bufferOrDrainOnEos(minReadLenPrefix, target, targetIndex, targetLength);
    245                if (eos || isDraining()) {
    246                    // if draining, readCount is a drain count
    247                    if (readCount != EOS) {
    248                        targetIndex += readCount;
    249                        targetLength -= readCount;
    250                    }
    251                    final int actual = targetIndex - targetIndexIn;
    252                    return actual > 0 ? actual : EOS;
    253                }
    254            }
    255        }
    256        // we found a variable start
    257        if (targetLength <= 0) {
    258            // no more room in target
    259            return targetLengthIn;
    260        }
    261        // SUFFIX
    262        // buffer more to find a balanced suffix
    263        final StringMatcher suffixMatcher = stringSubstitutor.getVariableSuffixMatcher();
    264        final int minReadLenSuffix = Math.max(minReadLenPrefix, suffixMatcher.size());
    265        readCount = buffer(readCount(minReadLenSuffix, pos));
    266        if (eos) {
    267            // EOS
    268            stringSubstitutor.replaceIn(buffer);
    269            toDrain = buffer.size();
    270            final int drainCount = drain(target, targetIndex, targetLength);
    271            return targetIndex + drainCount - targetIndexIn;
    272        }
    273        // buffer and break out when we find the end or a balanced suffix
    274        while (true) {
    275            if (isBufferMatchAt(suffixMatcher, pos)) {
    276                balance--;
    277                pos++;
    278                if (balance == 0) {
    279                    break;
    280                }
    281            } else if (isBufferMatchAt(prefixMatcher, pos)) {
    282                balance++;
    283                pos += prefixMatcher.size();
    284            } else if (isBufferMatchAt(prefixEscapeMatcher, pos)) {
    285                balance++;
    286                pos += prefixEscapeMatcher.size();
    287            } else {
    288                pos++;
    289            }
    290            readCount = buffer(readCount(minReadLenSuffix, pos));
    291            if (readCount == EOS && pos >= buffer.size()) {
    292                break;
    293            }
    294        }
    295        // substitute
    296        final int endPos = pos + 1;
    297        final int leftover = Math.max(0, buffer.size() - pos);
    298        stringSubstitutor.replaceIn(buffer, 0, Math.min(buffer.size(), endPos));
    299        pos = buffer.size() - leftover;
    300        final int drainLen = Math.min(targetLength, pos);
    301        // only drain up to what we've substituted
    302        toDrain = pos;
    303        drain(target, targetIndex, drainLen);
    304        return targetIndex - targetIndexIn + drainLen;
    305    }
    306
    307    /**
    308     * Returns how many chars to attempt reading to have room in the buffer for {@code count} chars starting at position
    309     * {@code pos}.
    310     */
    311    private int readCount(final int count, final int pos) {
    312        final int avail = buffer.size() - pos;
    313        return avail >= count ? 0 : count - avail;
    314    }
    315
    316}