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;
023
024import org.apache.commons.text.StringSubstitutor;
025import org.apache.commons.text.TextStringBuilder;
026import org.apache.commons.text.matcher.StringMatcher;
027import org.apache.commons.text.matcher.StringMatcherFactory;
028
029/**
030 * A {@link Reader} that performs string substitution on a source {@code Reader} using a {@link StringSubstitutor}.
031 *
032 * <p>
033 * Using this Reader avoids reading a whole file into memory as a {@code String} to perform string substitution, for
034 * example, when a Servlet filters a file to a client.
035 * </p>
036 * <p>
037 * This class is not thread-safe.
038 * </p>
039 *
040 * @since 1.9
041 */
042public class StringSubstitutorReader extends FilterReader {
043
044    /** The end-of-stream character marker. */
045    private static final int EOS = -1;
046
047    /** Our internal buffer. */
048    private final TextStringBuilder buffer = new TextStringBuilder();
049
050    /** End-of-Stream flag. */
051    private boolean eos;
052
053    /** Matches escaped variable starts. */
054    private final StringMatcher prefixEscapeMatcher;
055
056    /** Internal buffer for {@link #read()} method. */
057    private final char[] read1CharBuffer = {0};
058
059    /** The underlying StringSubstitutor. */
060    private final StringSubstitutor stringSubstitutor;
061
062    /** We don't always want to drain the whole buffer. */
063    private int toDrain;
064
065    /**
066     * Constructs a new instance.
067     *
068     * @param reader the underlying reader containing the template text known to the given {@code StringSubstitutor}.
069     * @param stringSubstitutor How to replace as we read.
070     * @throws NullPointerException if {@code reader} is {@code null}.
071     * @throws NullPointerException if {@code stringSubstitutor} is {@code null}.
072     */
073    public StringSubstitutorReader(final Reader reader, final StringSubstitutor stringSubstitutor) {
074        super(reader);
075        this.stringSubstitutor = new StringSubstitutor(stringSubstitutor);
076        this.prefixEscapeMatcher = StringMatcherFactory.INSTANCE.charMatcher(stringSubstitutor.getEscapeChar())
077            .andThen(stringSubstitutor.getVariablePrefixMatcher());
078    }
079
080    /**
081     * Buffers the requested number of characters if available.
082     */
083    private int buffer(final int requestReadCount) throws IOException {
084        final int actualReadCount = buffer.readFrom(super.in, requestReadCount);
085        eos = actualReadCount == EOS;
086        return actualReadCount;
087    }
088
089    /**
090     * Reads a requested number of chars from the underlying reader into the buffer. On EOS, set the state is DRAINING,
091     * drain, and return a drain count, otherwise, returns the actual read count.
092     */
093    private int bufferOrDrainOnEos(final int requestReadCount, final char[] target, final int targetIndex,
094        final int targetLength) throws IOException {
095        final int actualReadCount = buffer(requestReadCount);
096        return drainOnEos(actualReadCount, target, targetIndex, targetLength);
097    }
098
099    /**
100     * Drains characters from our buffer to the given {@code target}.
101     */
102    private int drain(final char[] target, final int targetIndex, final int targetLength) {
103        final int actualLen = Math.min(buffer.length(), targetLength);
104        final int drainCount = buffer.drainChars(0, actualLen, target, targetIndex);
105        toDrain -= drainCount;
106        if (buffer.isEmpty() || toDrain == 0) {
107            // nothing or everything drained.
108            toDrain = 0;
109        }
110        return drainCount;
111    }
112
113    /**
114     * Drains from the buffer to the target only if we are at EOS per the input count. If input count is EOS, drain and
115     * returns the drain count, otherwise return the input count. If draining, the state is set to DRAINING.
116     */
117    private int drainOnEos(final int readCountOrEos, final char[] target, final int targetIndex,
118        final int targetLength) {
119        if (readCountOrEos == EOS) {
120            // At EOS, drain.
121            if (buffer.isNotEmpty()) {
122                toDrain = buffer.size();
123                return drain(target, targetIndex, targetLength);
124            }
125            return EOS;
126        }
127        return readCountOrEos;
128    }
129
130    /**
131     * Tests if our buffer matches the given string matcher at the given position in the buffer.
132     */
133    private boolean isBufferMatchAt(final StringMatcher stringMatcher, final int pos) {
134        return stringMatcher.isMatch(buffer, pos) == stringMatcher.size();
135    }
136
137    /**
138     * Tests if we are draining.
139     */
140    private boolean isDraining() {
141        return toDrain > 0;
142    }
143
144    /**
145     * Reads a single character.
146     *
147     * @return a character as an {@code int} or {@code -1} for end-of-stream.
148     * @throws IOException If an I/O error occurs
149     */
150    @Override
151    public int read() throws IOException {
152        int count = 0;
153        // ask until we get a char or EOS
154        do {
155            count = read(read1CharBuffer, 0, 1);
156            if (count == EOS) {
157                return EOS;
158            }
159            // keep on buffering
160        } while (count < 1);
161        return read1CharBuffer[0];
162    }
163
164    /**
165     * Reads characters into a portion of an array.
166     *
167     * @param target Target buffer.
168     * @param targetIndexIn Index in the target at which to start storing characters.
169     * @param targetLengthIn Maximum number of characters to read.
170     *
171     * @return The number of characters read, or -1 on end of stream.
172     * @throws IOException If an I/O error occurs
173     */
174    @Override
175    public int read(final char[] target, final int targetIndexIn, final int targetLengthIn) throws IOException {
176        // The whole thing is inefficient because we must look for a balanced suffix to match the starting prefix
177        // Trying to substitute an incomplete expression can perform replacements when it should not.
178        // At a high level:
179        // - if draining, drain until empty or target length hit
180        // - copy to target until we find a variable start
181        // - buffer until a balanced suffix is read, then substitute.
182        if (eos && buffer.isEmpty()) {
183            return EOS;
184        }
185        if (targetLengthIn <= 0) {
186            // short-circuit: ask nothing, give nothing
187            return 0;
188        }
189        // drain check
190        int targetIndex = targetIndexIn;
191        int targetLength = targetLengthIn;
192        if (isDraining()) {
193            // drain as much as possible
194            final int drainCount = drain(target, targetIndex, Math.min(toDrain, targetLength));
195            if (drainCount == targetLength) {
196                // drained length requested, target is full, can only do more in the next invocation
197                return targetLength;
198            }
199            // drained less than requested, target not full.
200            targetIndex += drainCount;
201            targetLength -= drainCount;
202        }
203        // BUFFER from the underlying reader
204        final int minReadLenPrefix = prefixEscapeMatcher.size();
205        // READ enough to test for an [optionally escaped] variable start
206        int readCount = buffer(readCount(minReadLenPrefix, 0));
207        if (buffer.length() < minReadLenPrefix && targetLength < minReadLenPrefix) {
208            // read less than minReadLenPrefix, no variable possible
209            final int drainCount = drain(target, targetIndex, targetLength);
210            targetIndex += drainCount;
211            final int targetSize = targetIndex - targetIndexIn;
212            return eos && targetSize <= 0 ? EOS : targetSize;
213        }
214        if (eos) {
215            // EOS
216            stringSubstitutor.replaceIn(buffer);
217            toDrain = buffer.size();
218            final int drainCount = drain(target, targetIndex, targetLength);
219            targetIndex += drainCount;
220            final int targetSize = targetIndex - targetIndexIn;
221            return eos && targetSize <= 0 ? EOS : targetSize;
222        }
223        // PREFIX
224        // buffer and drain until we find a variable start, escaped or plain.
225        int balance = 0;
226        final StringMatcher prefixMatcher = stringSubstitutor.getVariablePrefixMatcher();
227        int pos = 0;
228        while (targetLength > 0) {
229            if (isBufferMatchAt(prefixMatcher, 0)) {
230                balance = 1;
231                pos = prefixMatcher.size();
232                break;
233            }
234            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}