PlaceholderReplacingReader.java
/*-
* ========================LICENSE_START=================================
* flyway-core
* ========================================================================
* Copyright (C) 2010 - 2025 Red Gate Software Ltd
* ========================================================================
* Licensed 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.
* =========================LICENSE_END==================================
*/
package org.flywaydb.core.internal.parser;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.LoadableMigrationInfo;
import org.flywaydb.core.api.configuration.Configuration;
import java.io.FilterReader;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
public class PlaceholderReplacingReader extends FilterReader {
private final String prefix;
private final String suffix;
private final String separator;
private final CaseInsensitiveMap placeholders = new CaseInsensitiveMap();
private final StringBuilder buffer = new StringBuilder();
private String markBuffer;
private String replacement;
private int replacementPos;
private String markReplacement;
private int markReplacementPos;
private static class CaseInsensitiveMap extends HashMap<String, String> {
@Override
public void putAll(Map<? extends String, ? extends String> m) {
for (Map.Entry<? extends String, ? extends String> e : m.entrySet()) {
put(e.getKey(), e.getValue());
}
}
@Override
public String put(String key, String value) {
return super.put(key.toLowerCase(), value);
}
@Override
public String get(Object key) {
return super.get(key.toString().toLowerCase());
}
@Override
public boolean containsKey(Object key) {
return super.containsKey(key.toString().toLowerCase());
}
}
public PlaceholderReplacingReader(final String prefix,
final String suffix,
final String separator,
final Map<String, String> placeholders,
final Reader in) {
super(in);
this.prefix = prefix;
this.suffix = suffix;
this.separator = separator;
this.placeholders.putAll(placeholders);
}
public static PlaceholderReplacingReader create(Configuration configuration, ParsingContext parsingContext, Reader reader) {
Map<String, String> placeholders = new HashMap<>();
Map<String, String> configurationPlaceholders = configuration.getPlaceholders();
Map<String, String> parsingContextPlaceholders = parsingContext.getPlaceholders();
placeholders.putAll(configurationPlaceholders);
placeholders.putAll(parsingContextPlaceholders);
return new PlaceholderReplacingReader(
configuration.getPlaceholderPrefix(),
configuration.getPlaceholderSuffix(),
configuration.getPlaceholderSeparator(),
placeholders,
reader);
}
public static PlaceholderReplacingReader create(Configuration configuration, ParsingContext parsingContext, LoadableMigrationInfo info) {
Map<String, String> placeholders = new HashMap<>();
Map<String, String> configurationPlaceholders = configuration.getPlaceholders();
Map<String, String> parsingContextPlaceholders = parsingContext.getPlaceholders();
final boolean placeholderReplacement = info.isPlaceholderReplacement() == null
? configuration.isPlaceholderReplacement()
: info.isPlaceholderReplacement();
if (placeholderReplacement) {
placeholders.putAll(configurationPlaceholders);
placeholders.putAll(parsingContextPlaceholders);
}
return new PlaceholderReplacingReader(
configuration.getPlaceholderPrefix(),
configuration.getPlaceholderSuffix(),
configuration.getPlaceholderSeparator(),
placeholders,
info.getLoadableResource().read());
}
public static PlaceholderReplacingReader createForScriptMigration(Configuration configuration, ParsingContext parsingContext, Reader reader) {
Map<String, String> placeholders = new HashMap<>();
Map<String, String> configurationPlaceholders = configuration.getPlaceholders();
Map<String, String> parsingContextPlaceholders = parsingContext.getPlaceholders();
placeholders.putAll(configurationPlaceholders);
placeholders.putAll(parsingContextPlaceholders);
return new PlaceholderReplacingReader(
configuration.getScriptPlaceholderPrefix(),
configuration.getScriptPlaceholderSuffix(),
"_",
placeholders,
reader);
}
@Override
public int read() throws IOException {
if (replacement == null) {
// if we have a previous read, then consume it
if (buffer.length() > 0) {
char c = buffer.charAt(0);
buffer.deleteCharAt(0);
return c;
}
// else read ahead by the prefix length
int r;
do {
r = super.read();
if (r == -1) {
break;
}
buffer.append((char) r);
} while (buffer.length() < prefix.length() && endsWith(buffer, prefix.substring(0, buffer.length())));
// if the buffer does not contain the prefix
if (!endsWith(buffer, prefix)) {
// if it contain data, return the first character of it
if (buffer.length() > 0) {
char c = buffer.charAt(0);
buffer.deleteCharAt(0);
return c;
}
// else return -1
return -1;
}
// if the buffer contained the prefix, wipe the buffer
buffer.delete(0, buffer.length());
// begin reading ahead until we get to the suffix
StringBuilder placeholderBuilder = new StringBuilder();
do {
int r1 = super.read();
if (r1 == -1) {
break;
} else {
placeholderBuilder.append((char) r1);
}
} while (!endsWith(placeholderBuilder, suffix));
// delete the suffix from the builder
for (int i = 0; i < suffix.length(); i++) {
placeholderBuilder.deleteCharAt(placeholderBuilder.length() - 1);
}
// look up the placeholder string
String placeholder = placeholderBuilder.toString();
if (!placeholders.containsKey(placeholder)) {
String canonicalPlaceholder = prefix + placeholder + suffix;
if (placeholder.startsWith("flyway" + separator)) {
throw new FlywayException("Failed to populate value for default placeholder: "
+ canonicalPlaceholder);
}
throw new FlywayException("No value provided for placeholder: "
+ canonicalPlaceholder
+ ". Check your configuration!");
}
// set the current placeholder replacement
replacement = placeholders.get(placeholder);
// Empty placeholder value -> move to the next character
if (replacement == null || replacement.length() == 0) {
replacement = null;
return read();
}
}
int result = replacement.charAt(replacementPos);
replacementPos++;
if (replacementPos >= replacement.length()) {
replacement = null;
replacementPos = 0;
}
return result;
}
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
int count = 0;
for (int i = 0; i < len; i++) {
int r = read();
if (r == -1) {
return count == 0 ? -1 : count;
}
cbuf[off + i] = (char) r;
count++;
}
return count;
}
@Override
public void mark(int readAheadLimit) throws IOException {
markBuffer = buffer.toString();
markReplacement = replacement;
markReplacementPos = replacementPos;
super.mark(readAheadLimit);
}
@Override
public void reset() throws IOException {
super.reset();
buffer.delete(0, buffer.length());
buffer.append(markBuffer);
replacement = markReplacement;
replacementPos = markReplacementPos;
}
private boolean endsWith(StringBuilder result, String str) {
if (result.length() < str.length()) {
return false;
}
for (int i = 0; i < str.length(); i++) {
if (result.charAt(result.length() - str.length() + i) != str.charAt(i)) {
return false;
}
}
return true;
}
}