/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.hawkular.alerts.engine.util;
import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.CharBuffer;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
/**
* Copied from http://tutorials.jenkov.com/java-howto/replace-strings-in-streams-arrays-files.html
* with fixes to {@link #read(char[], int, int)} and added support for escaping.
* <p>
* The following constructs have been added:
* <ul>
* <li>{@code ${p:v}} if token {@code p} is not found in the backing map, the provided value {@code v} is used
* instead of keeping the literal "${p}" in the output.</li>
* <li>{@code ${p1,p2:v}} if token {@code p1} is not found in the backing map, the expression is replaced by the
* value of token {@code p2} or value {@code v} if {@code p2} is not present either.</li>
* </ul>
* @author Lukas Krejci
*/
public class TokenReplacingReader extends Reader {
private PushbackReader pushbackReader = null;
private Map<String, String> tokens = null;
private StringBuilder tokenNameBuffer = new StringBuilder();
private String tokenValue = null;
private int tokenValueIndex = 0;
private boolean escaping = false;
private Deque<String> activeTokens;
private Map<String, String> resolvedTokens;
public TokenReplacingReader(String source, Map<String, String> tokens) {
this.pushbackReader = new PushbackReader(new StringReader(source), 2);
this.tokens = tokens;
this.activeTokens = new ArrayDeque<String>();
this.resolvedTokens = new HashMap<String, String>();
}
public TokenReplacingReader(String source, Map<String, String> tokens, Deque<String> activeTokens,
Map<String, String> resolvedTokens) {
pushbackReader = new PushbackReader(new StringReader(source));
this.tokens = tokens;
this.activeTokens = activeTokens;
this.resolvedTokens = resolvedTokens;
}
public int read(CharBuffer target) throws IOException {
throw new RuntimeException("Operation Not Supported");
}
public int read() throws IOException {
if (this.tokenValue != null) {
if (this.tokenValueIndex < this.tokenValue.length()) {
return this.tokenValue.charAt(this.tokenValueIndex++);
}
if (this.tokenValueIndex == this.tokenValue.length()) {
this.tokenValue = null;
this.tokenValueIndex = 0;
}
}
int data = this.pushbackReader.read();
if (escaping) {
escaping = false;
return data;
}
//only escape the ${ sequence.
//I.e. \${ is escaped as ${, but \$ is transferred literally
if (data == '\\') {
data = pushbackReader.read();
if (data != '$') {
pushbackReader.unread(data);
return '\\';
} else {
data = pushbackReader.read();
pushbackReader.unread(data);
if (data != '{') {
pushbackReader.unread('$');
return '\\';
} else {
escaping = true;
return '$';
}
}
}
if (data != '$')
return data;
data = this.pushbackReader.read();
if (data != '{') {
this.pushbackReader.unread(data);
return '$';
}
this.tokenNameBuffer.delete(0, this.tokenNameBuffer.length());
String tokenName;
boolean skipUntilExpressionEnd = false;
boolean nameIsValue = false;
boolean cont = true;
//0 - reading name
//1 - reading value
//2 - escape in value
int state = 0;
while (cont) {
data = this.pushbackReader.read();
switch (state) {
case 0: //reading name
switch (data) {
case ',':
if (skipUntilExpressionEnd) {
break;
}
//we've read the name, try if it is available.
//if yes, skip everything else until '}' and start outputting the value
//if not, try the name specified after the ','
tokenName = tokenNameBuffer.toString();
if (tokens.containsKey(tokenName)) {
skipUntilExpressionEnd = true;
} else {
//let's try the next token
tokenNameBuffer.delete(0, tokenNameBuffer.length());
}
break;
case ':':
if (skipUntilExpressionEnd) {
state = 1; //reading value
break;
}
if (tokenNameBuffer.length() == 0) {
//leading : is considered a part of the name
tokenNameBuffer.append((char) data);
} else {
state = 1; //reading value
tokenName = tokenNameBuffer.toString();
if (tokens.containsKey(tokenName)) {
skipUntilExpressionEnd = true;
} else {
tokenNameBuffer.delete(0, tokenNameBuffer.length());
nameIsValue = true;
}
}
break;
case '}':
cont = false;
break;
default:
this.tokenNameBuffer.append((char) data);
}
break;
case 1: //reading value
switch (data) {
case '\\':
data = pushbackReader.read();
if (data != '}') {
pushbackReader.unread(data);
data = '\\';
}
if (nameIsValue) {
tokenNameBuffer.append((char) data);
}
break;
case '}':
cont = false;
break;
default:
if (nameIsValue) {
tokenNameBuffer.append((char) data);
}
}
}
}
tokenName = tokenNameBuffer.toString();
if (nameIsValue) {
TokenReplacingReader childReader = new TokenReplacingReader(tokenName, tokens, activeTokens,
resolvedTokens);
tokenValue = readAll(childReader);
} else {
tokenValue = resolveToken(tokenName);
}
tokenValueIndex = 0;
if (!this.tokenValue.isEmpty()) {
return this.tokenValue.charAt(this.tokenValueIndex++);
} else {
return read();
}
}
public int read(char[] cbuf) throws IOException {
return read(cbuf, 0, cbuf.length);
}
public int read(char[] cbuf, int off, int len) throws IOException {
int i = 0;
for (; i < len; i++) {
int nextChar = read();
if (nextChar == -1) {
if (i == 0) {
i = -1;
}
break;
}
cbuf[off + i] = (char) nextChar;
}
return i;
}
public void close() throws IOException {
this.pushbackReader.close();
}
public long skip(long n) throws IOException {
throw new UnsupportedOperationException("skip() not supported on TokenReplacingReader.");
}
public boolean ready() throws IOException {
return this.pushbackReader.ready();
}
public boolean markSupported() {
return false;
}
public void mark(int readAheadLimit) throws IOException {
throw new IOException("mark() not supported on TokenReplacingReader.");
}
public void reset() throws IOException {
throw new IOException("reset() not supported on TokenReplacingReader.");
}
private String readAll(Reader r) throws IOException {
int c;
StringBuilder bld = new StringBuilder();
while((c = r.read()) >= 0) {
bld.append((char)c);
}
return bld.toString();
}
private String resolveToken(String tokenName) throws IOException {
if (resolvedTokens.containsKey(tokenName)) {
return resolvedTokens.get(tokenName);
}
if (activeTokens.contains(tokenName)) {
throw new IllegalArgumentException("Token '" + tokenName
+ "' (indirectly) contains reference to itself in its value.");
}
activeTokens.push(tokenName);
String tokenValue = tokens.get(tokenName);
if (tokenValue != null) {
if (tokenValue.contains("${")) {
TokenReplacingReader childReader = new TokenReplacingReader(tokenValue, tokens, activeTokens,
resolvedTokens);
tokenValue = readAll(childReader);
}
} else {
tokenValue = "${" + tokenName + "}";
}
resolvedTokens.put(tokenName, tokenValue);
activeTokens.pop();
return tokenValue;
}
}