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     * @return The number of characters read, or -1 on end of stream.
171     * @throws IOException If an I/O error occurs
172     */
173    @Override
174    public int read(final char[] target, final int targetIndexIn, final int targetLengthIn) throws IOException {
175        // The whole thing is inefficient because we must look for a balanced suffix to match the starting prefix
176        // Trying to substitute an incomplete expression can perform replacements when it should not.
177        // At a high level:
178        // - if draining, drain until empty or target length hit
179        // - copy to target until we find a variable start
180        // - buffer until a balanced suffix is read, then substitute.
181        if (eos && buffer.isEmpty()) {
182            return EOS;
183        }
184        if (targetLengthIn <= 0) {
185            // short-circuit: ask nothing, give nothing
186            return 0;
187        }
188        // drain check
189        int targetIndex = targetIndexIn;
190        int targetLength = targetLengthIn;
191        if (isDraining()) {
192            // drain as much as possible
193            final int drainCount = drain(target, targetIndex, Math.min(toDrain, targetLength));
194            if (drainCount == targetLength) {
195                // drained length requested, target is full, can only do more in the next invocation
196                return targetLength;
197            }
198            // drained less than requested, target not full.
199            targetIndex += drainCount;
200            targetLength -= drainCount;
201        }
202        // BUFFER from the underlying reader
203        final int minReadLenPrefix = prefixEscapeMatcher.size();
204        // READ enough to test for an [optionally escaped] variable start
205        int readCount = buffer(readCount(minReadLenPrefix, 0));
206        if (buffer.length() < minReadLenPrefix && targetLength < minReadLenPrefix) {
207            // read less than minReadLenPrefix, no variable possible
208            final int drainCount = drain(target, targetIndex, targetLength);
209            targetIndex += drainCount;
210            final int targetSize = targetIndex - targetIndexIn;
211            return eos && targetSize <= 0 ? EOS : targetSize;
212        }
213        if (eos) {
214            // EOS
215            stringSubstitutor.replaceIn(buffer);
216            toDrain = buffer.size();
217            final int drainCount = drain(target, targetIndex, targetLength);
218            targetIndex += drainCount;
219            final int targetSize = targetIndex - targetIndexIn;
220            return eos && targetSize <= 0 ? EOS : targetSize;
221        }
222        // PREFIX
223        // buffer and drain until we find a variable start, escaped or plain.
224        int balance = 0;
225        final StringMatcher prefixMatcher = stringSubstitutor.getVariablePrefixMatcher();
226        int pos = 0;
227        while (targetLength > 0) {
228            if (isBufferMatchAt(prefixMatcher, 0)) {
229                balance = 1;
230                pos = prefixMatcher.size();
231                break;
232            }
233            if (isBufferMatchAt(prefixEscapeMatcher, 0)) {
234                balance = 1;
235                pos = prefixEscapeMatcher.size();
236                break;
237            }
238            // drain first char
239            final int drainCount = drain(target, targetIndex, 1);
240            targetIndex += drainCount;
241            targetLength -= drainCount;
242            if (buffer.size() < minReadLenPrefix) {
243                readCount = bufferOrDrainOnEos(minReadLenPrefix, target, targetIndex, targetLength);
244                if (eos || isDraining()) {
245                    // if draining, readCount is a drain count
246                    if (readCount != EOS) {
247                        targetIndex += readCount;
248                        targetLength -= readCount;
249                    }
250                    final int actual = targetIndex - targetIndexIn;
251                    return actual > 0 ? actual : EOS;
252                }
253            }
254        }
255        // we found a variable start
256        if (targetLength <= 0) {
257            // no more room in target
258            return targetLengthIn;
259        }
260        // SUFFIX
261        // buffer more to find a balanced suffix
262        final StringMatcher suffixMatcher = stringSubstitutor.getVariableSuffixMatcher();
263        final int minReadLenSuffix = Math.max(minReadLenPrefix, suffixMatcher.size());
264        readCount = buffer(readCount(minReadLenSuffix, pos));
265        if (eos) {
266            // EOS
267            stringSubstitutor.replaceIn(buffer);
268            toDrain = buffer.size();
269            final int drainCount = drain(target, targetIndex, targetLength);
270            return targetIndex + drainCount - targetIndexIn;
271        }
272        // buffer and break out when we find the end or a balanced suffix
273        while (true) {
274            if (isBufferMatchAt(suffixMatcher, pos)) {
275                balance--;
276                pos++;
277                if (balance == 0) {
278                    break;
279                }
280            } else if (isBufferMatchAt(prefixMatcher, pos)) {
281                balance++;
282                pos += prefixMatcher.size();
283            } else if (isBufferMatchAt(prefixEscapeMatcher, pos)) {
284                balance++;
285                pos += prefixEscapeMatcher.size();
286            } else {
287                pos++;
288            }
289            readCount = buffer(readCount(minReadLenSuffix, pos));
290            if (readCount == EOS && pos >= buffer.size()) {
291                break;
292            }
293        }
294        // substitute
295        final int endPos = pos + 1;
296        final int leftover = Math.max(0, buffer.size() - pos);
297        stringSubstitutor.replaceIn(buffer, 0, Math.min(buffer.size(), endPos));
298        pos = buffer.size() - leftover;
299        final int drainLen = Math.min(targetLength, pos);
300        // only drain up to what we've substituted
301        toDrain = pos;
302        drain(target, targetIndex, drainLen);
303        return targetIndex - targetIndexIn + drainLen;
304    }
305
306    /**
307     * Returns how many chars to attempt reading to have room in the buffer for {@code count} chars starting at position
308     * {@code pos}.
309     */
310    private int readCount(final int count, final int pos) {
311        final int avail = buffer.size() - pos;
312        return avail >= count ? 0 : count - avail;
313    }
314
315}