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}