/*
* -----------------------------------------------------------------------\
* PerfCake
*
* Copyright (C) 2010 - 2016 the original author or authors.
*
* 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.perfcake.util;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
/**
* Holds a template capable of replacing properties in form of ${property} and @{property} to their values.
* The properties with the dollar sign are replaced only once, while the properties with the "at" sign are replaced with each call
* to {@link #toString(Properties)} with the current values (this simulates JavaEE EL). System properties can be accessed using the props. prefix,
* and environment properties can be accessed using the env. prefix.
* Automatically provides environment properties, system properties and user specified properties.
* Default values are separated by semicolon (e.g. ${property:defaultValue}). The property name must contain only letters, numbers and underscores
* (this is not strictly checked but may lead to undefined behaviour).
* Backslash works as a general escape character and escapes any letter behind it (e.g. \\ is replaced by \, \@ is replaced by @ etc.).
* Examples: ${propertyA} ${non_existing:default} ${env.JAVA_HOME} ${props['java.runtime.name']}
* Notice: The first call to the constructor and calls to the static method {@link #parseTemplate(String, Properties)} might take more time than a simple RegExp
* match but this is payed back for the subsequent calls to {@link #toString()}.
*
* @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a>
*/
public class StringTemplate implements Serializable {
/**
* Serial version UID.
*/
private static final long serialVersionUID = -1263887679189281564L;
/**
* Global properties passed in while creating the template.
*/
private Properties properties;
/**
* Compiled data of the template. Static parts of the template.
*/
private String[] parts;
/**
* Compiled data of the template. Property names to be replaced.
*/
private String[] replacements;
/**
* Compiled data of the template. Default values of properties.
*/
private String[] defaults;
/**
* Compiled data of the template. Number of replacements in the template.
*/
private int patternSize;
/**
* True when there were any placeholders in the template.
*/
private boolean hasPlaceholders = false;
/**
* Creates a template using the provided string interpretation.
*
* @param template
* The string interpretation of the pattern.
*/
public StringTemplate(final String template) {
this(template, null);
}
/**
* Creates a template using the provided string interpretation using the additional properties.
*
* @param template
* The string interpretation of the pattern.
* @param properties
* Properties to be immediately replaced in the template.
*/
public StringTemplate(final String template, final Properties properties) {
this.properties = properties == null ? new Properties() : properties;
compilePattern(firstPhase(template));
}
/**
* Were there any placeholders in the template?
*
* @return True if and only if there were any placeholders in the template.
*/
public boolean hasPlaceholders() {
return hasPlaceholders;
}
/**
* Were there any placeholders that need to be replaced each time when rendered?
*
* @return True if and only if there were any placeholders that need to be replaced each time when rendered.
*/
public boolean hasDynamicPlaceholders() {
return patternSize > 0;
}
/**
* Renders the template.
*
* @return The rendered template.
*/
public String toString() {
return toString(null);
}
/**
* Renders the template using the additionally provided properties.
*
* @param localProperties
* The additional properties to be replaced in the template.
* @return The rendered template.
*/
public String toString(final Properties localProperties) {
final StringBuilder result = new StringBuilder(parts[0]);
for (int i = 0; i < patternSize; i++) {
if (localProperties == null) {
result.append(getProperty(replacements[i], this.properties, defaults[i]));
} else {
final String globalProperty = this.properties.getProperty(replacements[i]);
result.append(getProperty(replacements[i], localProperties, globalProperty == null ? defaults[i] : globalProperty));
}
result.append(parts[i + 1]);
}
return result.toString();
}
/**
* Gets the rendered template of the provided string interpretation and properties.
*
* @param template
* The string representation of the template.
* @param properties
* The properties to be replaced in the template.
* @return The rendered template.
*/
public static String parseTemplate(final String template, final Properties properties) {
return new StringTemplate(template, properties).toString(properties);
}
/**
* Reads the property name from inside ${...} placeholder.
*
* @param mill
* The string mill crunching the template.
* @param buffer
* The output buffer to append the property name.
*/
private void readPropertyName(final StringMill mill, final StringBuilder buffer) {
hasPlaceholders = true; // we are reading a property now
while (!mill.end() && mill.cur() != '}') {
if (mill.cur() == '\\' || mill.cur() == '\u0001') {
buffer.append(mill.next());
mill.cut();
mill.cut();
} else {
buffer.append(mill.cur());
mill.step();
}
}
}
/**
* Pre-compiles the template string replacing all ${...} and preserving escaped characters.
*
* @param template
* The template to be compiled.
* @return The pre-compiled template.
*/
private String firstPhase(final String template) {
final StringMill mill = new StringMill(template);
final StringBuilder result = new StringBuilder();
StringBuilder buffer = new StringBuilder();
while (!mill.end()) {
if (mill.cur() == '\\' && mill.next() != '@' && (mill.next() == '\\' || mill.next() == '{' || mill.next() == '}' || mill.next() == '$')) {
result.append(mill.next());
mill.cut();
mill.cut();
} else if (mill.cur() == '\\' && mill.next() == '@') { // let's mark the escaped @ for the second round
result.append('\u0001');
result.append('@');
mill.cut();
mill.cut();
} else if (mill.pre() != '\\' && mill.cur() == '$' && mill.next() == '{') {
mill.step();
mill.step();
readPropertyName(mill, buffer);
if (mill.cur() == '}') {
mill.step();
final String[] curly = buffer.toString().split(":", 2);
String value = getProperty(curly[0], properties);
if (value == null && curly.length > 1) {
value = curly[1];
}
result.append(value);
} else {
result.append("${");
result.append(buffer);
}
buffer = new StringBuilder();
} else {
result.append(mill.cur());
mill.step();
}
}
if (buffer.length() > 0) {
result.append(buffer);
}
return result.toString();
}
/**
* Second phase of pattern processing, compiles the needed data structures to be able to fast render the template.
*
* @param template
* The pre-compiled template.
*/
private void compilePattern(final String template) {
final List<String> parts = new LinkedList<>();
final List<String> replacements = new LinkedList<>();
final List<String> defaults = new LinkedList<>();
final StringMill mill = new StringMill(template);
StringBuilder result = new StringBuilder();
StringBuilder buffer = new StringBuilder();
while (!mill.end()) {
if (mill.cur() == '\u0001' && mill.next() == '@') {
result.append(mill.next());
mill.cut();
mill.cut();
} else if (mill.cur() == '@' && mill.next() == '{') {
mill.step();
mill.step();
readPropertyName(mill, buffer);
if (mill.cur() == '}') {
mill.step();
final String[] curly = buffer.toString().split(":", 2);
parts.add(result.toString());
result = new StringBuilder();
replacements.add(curly[0]);
defaults.add(curly.length > 1 ? curly[1] : null);
} else {
result.append("@{");
result.append(buffer);
}
buffer = new StringBuilder();
} else {
result.append(mill.cur());
mill.step();
}
}
if (buffer.length() > 0) {
result.append(buffer);
}
if (result.length() > 0) {
parts.add(result.toString());
} else {
parts.add(""); // avoid out of bounds exception later
}
this.parts = parts.toArray(new String[parts.size()]);
this.replacements = replacements.toArray(new String[replacements.size()]);
this.defaults = defaults.toArray(new String[defaults.size()]);
this.patternSize = replacements.size();
}
/**
* Gets the property value from all known places given the property prefix.
*
* @param property
* The property name.
* @param properties
* Local properties to be used for default values.
* @return The property value, or null if the property was not found in any location.
*/
private static String getProperty(final String property, final Properties properties) {
if (property.startsWith("env.")) {
return System.getenv(property.substring(4));
}
if (property.startsWith("props['")) {
return System.getProperty(property.substring(7, property.length() - 2)); // we expect it to end with ']
}
if (property.startsWith("props[")) {
return System.getProperty(property.substring(6, property.length() - 1));
}
if (property.startsWith("props.")) {
return System.getProperty(property.substring(6));
}
return properties != null ? properties.getProperty(property) : null;
}
/**
* Gets the property value from all known places given the property prefix.
*
* @param property
* The property name.
* @param properties
* Local properties to be used when system resources did not contain the property.
* @param defaultValue
* The default value to be used when the property was not found anywhere.
* @return The property value, or null if the property was not found in any location.
*/
private static String getProperty(final String property, final Properties properties, final String defaultValue) {
final String value = getProperty(property, properties);
return value == null ? defaultValue : value;
}
/**
* Helper to crunch through a string.
*/
private static class StringMill {
/**
* The string being crunched.
*/
private final String str;
/**
* Current position in the string.
*/
private int i = 0;
/**
* Is the previous character masked? Simulates consumption of the previous character.
*/
private boolean preNull = false;
/**
* Start crunching the given string.
*
* @param str
* String to be crunched.
*/
private StringMill(final String str) {
this.str = str;
}
/**
* Character relative to the current position.
*
* @param rel
* Index relative to the current position.
* @return The character from the corresponding index or \u0000 if the index is out of string boundaries.
*/
private char charRel(final int rel) {
if (i + rel < 0 || i + rel >= str.length()) {
return 0;
}
return str.charAt(i + rel);
}
/**
* Skip the character and move to the next position.
*/
private void step() {
i++;
preNull = false;
}
/**
* Consume the character and move to the next position. The character cannot be later obtained by the {@link #pre()} method.
*/
private void cut() {
i++;
preNull = true;
}
/**
* Did we reach the end of the string?
*
* @return True if and only if we hit the end of the string.
*/
private boolean end() {
return i >= str.length();
}
/**
* Gets the character at the current position.
*
* @return The character at the current position.
*/
private char cur() {
return charRel(0);
}
/**
* Gets the character at the previous position.
*
* @return The character at the previous position.
*/
private char pre() {
return preNull ? 0 : charRel(-1);
}
/**
* Gets the character at the next position.
*
* @return The character at the next position.
*/
private char next() {
return charRel(1);
}
@Override
public String toString() {
return "StringMill: " + pre() + " --->" + cur() + " " + next();
}
}
}