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}