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

org.apache.commons.text.io.StringSubstitutorReader Maven / Gradle / Ivy

There is a newer version: 1.11.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.text.io;

import java.io.FilterReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Objects;

import org.apache.commons.text.StringSubstitutor;
import org.apache.commons.text.TextStringBuilder;
import org.apache.commons.text.matcher.StringMatcher;
import org.apache.commons.text.matcher.StringMatcherFactory;

/**
 * A {@link Reader} that performs string substitution on a source {@code Reader} using a {@link StringSubstitutor}.
 *
 * 

* Using this Reader avoids reading a whole file into memory as a {@code String} to perform string substitution, for * example, when a Servlet filters a file to a client. *

*

* This class is not thread-safe. *

* * @since 1.9 */ public class StringSubstitutorReader extends FilterReader { /** The end-of-stream character marker. */ private static final int EOS = -1; /** Our internal buffer. */ private final TextStringBuilder buffer = new TextStringBuilder(); /** End-of-Stream flag. */ private boolean eos; /** Matches escaped variable starts. */ private final StringMatcher prefixEscapeMatcher; /** Internal buffer for {@link #read()} method. */ private final char[] read1CharBuffer = {0}; /** The underlying StringSubstitutor. */ private final StringSubstitutor stringSubstitutor; /** We don't always want to drain the whole buffer. */ private int toDrain; /** * Constructs a new instance. * * @param reader the underlying reader containing the template text known to the given {@code StringSubstitutor}. * @param stringSubstitutor How to replace as we read. * @throws NullPointerException if {@code reader} is {@code null}. * @throws NullPointerException if {@code stringSubstitutor} is {@code null}. */ public StringSubstitutorReader(final Reader reader, final StringSubstitutor stringSubstitutor) { super(reader); this.stringSubstitutor = Objects.requireNonNull(stringSubstitutor); this.prefixEscapeMatcher = StringMatcherFactory.INSTANCE.charMatcher(stringSubstitutor.getEscapeChar()) .andThen(stringSubstitutor.getVariablePrefixMatcher()); } /** * Buffers the requested number of characters if available. */ private int buffer(final int requestReadCount) throws IOException { final int actualReadCount = buffer.readFrom(super.in, requestReadCount); eos = actualReadCount == EOS; return actualReadCount; } /** * Reads a requested number of chars from the underlying reader into the buffer. On EOS, set the state is DRAINING, * drain, and return a drain count, otherwise, returns the actual read count. */ private int bufferOrDrainOnEos(final int requestReadCount, final char[] target, final int targetIndex, final int targetLength) throws IOException { final int actualReadCount = buffer(requestReadCount); return drainOnEos(actualReadCount, target, targetIndex, targetLength); } /** * Drains characters from our buffer to the given {@code target}. */ private int drain(final char[] target, final int targetIndex, final int targetLength) { final int actualLen = Math.min(buffer.length(), targetLength); final int drainCount = buffer.drainChars(0, actualLen, target, targetIndex); toDrain -= drainCount; if (buffer.isEmpty() || toDrain == 0) { // nothing or everything drained. toDrain = 0; } return drainCount; } /** * Drains from the buffer to the target only if we are at EOS per the input count. If input count is EOS, drain and * returns the drain count, otherwise return the input count. If draining, the state is set to DRAINING. */ private int drainOnEos(final int readCountOrEos, final char[] target, final int targetIndex, final int targetLength) { if (readCountOrEos == EOS) { // At EOS, drain. if (buffer.isNotEmpty()) { toDrain = buffer.size(); return drain(target, targetIndex, targetLength); } return EOS; } return readCountOrEos; } /** * Tests if our buffer matches the given string matcher at the given position in the buffer. */ private boolean isBufferMatchAt(final StringMatcher stringMatcher, final int pos) { return stringMatcher.isMatch(buffer, pos) == stringMatcher.size(); } /** * Tests if we are draining. */ private boolean isDraining() { return toDrain > 0; } /** * Reads a single character. * * @return a character as an {@code int} or {@code -1} for end-of-stream. * @throws IOException If an I/O error occurs */ @Override public int read() throws IOException { int count = 0; // ask until we get a char or EOS do { count = read(read1CharBuffer, 0, 1); if (count == EOS) { return EOS; } // keep on buffering } while (count < 1); return read1CharBuffer[0]; } /** * Reads characters into a portion of an array. * * @param target Target buffer. * @param targetIndexIn Index in the target at which to start storing characters. * @param targetLengthIn Maximum number of characters to read. * * @return The number of characters read, or -1 on end of stream. * @throws IOException If an I/O error occurs */ @Override public int read(final char[] target, final int targetIndexIn, final int targetLengthIn) throws IOException { // The whole thing is inefficient because we must look for a balanced suffix to match the starting prefix // Trying to substitute an incomplete expression can perform replacements when it should not. // At a high level: // - if draining, drain until empty or target length hit // - copy to target until we find a variable start // - buffer until a balanced suffix is read, then substitute. if (eos && buffer.isEmpty()) { return EOS; } if (targetLengthIn <= 0) { // short-circuit: ask nothing, give nothing return 0; } // drain check int targetIndex = targetIndexIn; int targetLength = targetLengthIn; if (isDraining()) { // drain as much as possible final int drainCount = drain(target, targetIndex, Math.min(toDrain, targetLength)); if (drainCount == targetLength) { // drained length requested, target is full, can only do more in the next invocation return targetLength; } // drained less than requested, target not full. targetIndex += drainCount; targetLength -= drainCount; } // BUFFER from the underlying reader final int minReadLenPrefix = prefixEscapeMatcher.size(); // READ enough to test for an [optionally escaped] variable start int readCount = buffer(readCount(minReadLenPrefix, 0)); if (buffer.length() < minReadLenPrefix && targetLength < minReadLenPrefix) { // read less than minReadLenPrefix, no variable possible final int drainCount = drain(target, targetIndex, targetLength); targetIndex += drainCount; final int targetSize = targetIndex - targetIndexIn; return eos && targetSize <= 0 ? EOS : targetSize; } if (eos) { // EOS stringSubstitutor.replaceIn(buffer); toDrain = buffer.size(); final int drainCount = drain(target, targetIndex, targetLength); targetIndex += drainCount; final int targetSize = targetIndex - targetIndexIn; return eos && targetSize <= 0 ? EOS : targetSize; } // PREFIX // buffer and drain until we find a variable start, escaped or plain. int balance = 0; final StringMatcher prefixMatcher = stringSubstitutor.getVariablePrefixMatcher(); int pos = 0; while (targetLength > 0) { if (isBufferMatchAt(prefixMatcher, 0)) { balance = 1; pos = prefixMatcher.size(); break; } else if (isBufferMatchAt(prefixEscapeMatcher, 0)) { balance = 1; pos = prefixEscapeMatcher.size(); break; } // drain first char final int drainCount = drain(target, targetIndex, 1); targetIndex += drainCount; targetLength -= drainCount; if (buffer.size() < minReadLenPrefix) { readCount = bufferOrDrainOnEos(minReadLenPrefix, target, targetIndex, targetLength); if (eos || isDraining()) { // if draining, readCount is a drain count if (readCount != EOS) { targetIndex += readCount; targetLength -= readCount; } final int actual = targetIndex - targetIndexIn; return actual > 0 ? actual : EOS; } } } // we found a variable start if (targetLength <= 0) { // no more room in target return targetLengthIn; } // SUFFIX // buffer more to find a balanced suffix final StringMatcher suffixMatcher = stringSubstitutor.getVariableSuffixMatcher(); final int minReadLenSuffix = Math.max(minReadLenPrefix, suffixMatcher.size()); readCount = buffer(readCount(minReadLenSuffix, pos)); if (eos) { // EOS stringSubstitutor.replaceIn(buffer); toDrain = buffer.size(); final int drainCount = drain(target, targetIndex, targetLength); return targetIndex + drainCount - targetIndexIn; } // buffer and break out when we find the end or a balanced suffix while (true) { if (isBufferMatchAt(suffixMatcher, pos)) { balance--; pos++; if (balance == 0) { break; } } else if (isBufferMatchAt(prefixMatcher, pos)) { balance++; pos += prefixMatcher.size(); } else if (isBufferMatchAt(prefixEscapeMatcher, pos)) { balance++; pos += prefixEscapeMatcher.size(); } else { pos++; } readCount = buffer(readCount(minReadLenSuffix, pos)); if (readCount == EOS && pos >= buffer.size()) { break; } } // substitute final int endPos = pos + 1; final int leftover = Math.max(0, buffer.size() - pos); stringSubstitutor.replaceIn(buffer, 0, Math.min(buffer.size(), endPos)); pos = buffer.size() - leftover; final int drainLen = Math.min(targetLength, pos); // only drain up to what we've substituted toDrain = pos; drain(target, targetIndex, drainLen); return targetIndex - targetIndexIn + drainLen; } /** * Returns how many chars to attempt reading to have room in the buffer for {@code count} chars starting at position * {@code pos}. */ private int readCount(final int count, final int pos) { final int avail = buffer.size() - pos; return avail >= count ? 0 : count - avail; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy