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}