/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Observable;
import java.util.Set;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorVersion;
import com.rapidminer.operator.Value;
import com.rapidminer.parameter.UndefinedMacroError;
import com.rapidminer.parameter.UndefinedParameterError;
import com.rapidminer.tools.Tools;
/**
* This class can be used to store macros for an process which can be defined by the operator
* {@link com.rapidminer.operator.MacroDefinitionOperator}. It also defines some standard macros
* like the process path or file name.
*
* @author Ingo Mierswa
*/
public class MacroHandler extends Observable {
public static final String PROCESS_NAME = "process_name";
public static final String PROCESS_FILE = "process_file";
public static final String PROCESS_PATH = "process_path";
public static final String PROCESS_START = "process_start";
/**
* Remaining problem is that predefined macros that are overridden by custom macros are
* evaluated first. The result is the predefined value.
*/
private static final String[] ALL_PREDEFINED_MACROS = { PROCESS_NAME, PROCESS_FILE, PROCESS_PATH, PROCESS_START, "a",
"execution_count", "b", "c", "n", "operator_name", "t", "p[]", "v[]" };
/** all predefined macros that do not depend on an operator except for v[] */
private static final Set<String> PREDEFINED_OPERATOR_INDEPENDENT_MACROS = new HashSet<>(Arrays.asList(
new String[] { PROCESS_NAME, PROCESS_FILE, PROCESS_PATH, PROCESS_START, Operator.STRING_EXPANSION_MACRO_TIME }));
/** all predefined macros that depend on an operator except for p[] */
private static final Set<String> PREDEFINED_OPERATOR_DEPENDENT_MACROS = new HashSet<>(
Arrays.asList(new String[] { Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_USER_FRIENDLY,
Operator.STRING_EXPANSION_MACRO_OPERATORNAME_USER_FRIENDLY, Operator.STRING_EXPANSION_MACRO_OPERATORNAME,
Operator.STRING_EXPANSION_MACRO_OPERATORCLASS, Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES,
Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_PLUS_ONE }));
private static final String[] ALL_USER_FRIENDLY_PREDEFINED_MACROS = { PROCESS_NAME, PROCESS_FILE, PROCESS_PATH,
PROCESS_START, Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_USER_FRIENDLY,
Operator.STRING_EXPANSION_MACRO_OPERATORNAME_USER_FRIENDLY };
private static final OperatorVersion THROW_ERROR_ON_UNDEFINED_MACRO = new OperatorVersion(6, 0, 3);
/**
* This HashSet contains the keys of legacy macros which will be replaced while string
* expansion. CAUTION: Do NOT add any new content to this set.
*/
private static final HashSet<String> LEGACY_STRING_EXPANSION_MACRO_KEYS = new HashSet<>();
static {
LEGACY_STRING_EXPANSION_MACRO_KEYS.add(Operator.STRING_EXPANSION_MACRO_OPERATORNAME);
LEGACY_STRING_EXPANSION_MACRO_KEYS.add(Operator.STRING_EXPANSION_MACRO_OPERATORCLASS);
LEGACY_STRING_EXPANSION_MACRO_KEYS.add(Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES);
LEGACY_STRING_EXPANSION_MACRO_KEYS.add(Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_PLUS_ONE);
LEGACY_STRING_EXPANSION_MACRO_KEYS.add(Operator.STRING_EXPANSION_MACRO_TIME);
LEGACY_STRING_EXPANSION_MACRO_KEYS.add(Operator.STRING_EXPANSION_MACRO_PERCENT_SIGN);
LEGACY_STRING_EXPANSION_MACRO_KEYS.add(Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_SHIFTED
+ Operator.STRING_EXPANSION_MACRO_PARAMETER_START);
LEGACY_STRING_EXPANSION_MACRO_KEYS
.add(Operator.STRING_EXPANSION_MACRO_OPERATORVALUE + Operator.STRING_EXPANSION_MACRO_PARAMETER_START);
}
// ThreadLocal because DateFormat is NOT threadsafe and creating a new DateFormat is
// EXTREMELY expensive
/**
* Used for formatting the %{process_start} and current time %{t} macro
*/
public static final ThreadLocal<DateFormat> DATE_FORMAT = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
// clone because getDateInstance uses an internal pool which can return the same
// instance for multiple threads
return new SimpleDateFormat("yyyy_MM_dd-a_KK_mm_ss");
}
};
/**
* This HashSet contains the keys of macros which will be replaced while string expansion. Each
* macro item might have an arbitrary length.
*/
private static final HashSet<String> STRING_EXPANSION_MACRO_KEYS = new HashSet<>();
static {
STRING_EXPANSION_MACRO_KEYS.add(Operator.STRING_EXPANSION_MACRO_OPERATORNAME_USER_FRIENDLY);
STRING_EXPANSION_MACRO_KEYS.add(Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_USER_FRIENDLY);
}
private final Process process;
private final Map<String, String> macroMap = new HashMap<>();
private final Object LOCK = new Object();
public MacroHandler(Process process) {
this.process = process;
}
public void clear() {
setChanged();
synchronized (LOCK) {
macroMap.clear();
}
notifyObservers(this);
}
public Iterator<String> getDefinedMacroNames() {
Iterator<String> iterator = null;
synchronized (LOCK) {
iterator = new HashMap<>(macroMap).keySet().iterator();
}
return iterator;
}
/**
* @return an array with the names of all user-friendly predefined macros available in
* RapidMiner
*/
public String[] getAllGraphicallySupportedPredefinedMacros() {
return ALL_USER_FRIENDLY_PREDEFINED_MACROS;
}
/**
* @return an array with the names of ALL predefined macros available in RapidMiner
*/
public String[] getAllPredefinedMacros() {
return ALL_PREDEFINED_MACROS;
}
/**
* Adds a macro to this MacroHandler. If a macro with this name is already present, it will be
* overwritten.
*
* @param macro
* The name of the macro.
* @param value
* The new value of the macro.
*/
public void addMacro(String macro, String value) {
if (macro != null && !macro.isEmpty()) {
setChanged();
synchronized (LOCK) {
macroMap.put(macro, value);
}
notifyObservers(this);
}
}
public void removeMacro(String macro) {
setChanged();
synchronized (LOCK) {
macroMap.remove(macro);
}
notifyObservers(this);
}
/**
* Checks whether a provided macro was set.
*
* @param macro
* the macro key
* @param operator
* the operator that can be used to resolve the macro
* @return <code>true</code> in case it was set, <code>false</code> otherwise
*/
public boolean isMacroSet(String macro, Operator operator) {
synchronized (LOCK) {
if (macroMap.containsKey(macro) || PREDEFINED_OPERATOR_INDEPENDENT_MACROS.contains(macro)) {
return true;
}
}
return operator != null && PREDEFINED_OPERATOR_DEPENDENT_MACROS.contains(macro);
}
/**
* Resolves the macros "process_name", "process_file", "process_path", "t" and user defined
* macros.
*/
public String getMacro(String macro) {
if (PREDEFINED_OPERATOR_INDEPENDENT_MACROS.contains(macro)) {
switch (macro) {
case PROCESS_NAME:
ProcessLocation processLocation = process.getProcessLocation();
if (processLocation == null) {
return null;
}
if (processLocation instanceof FileProcessLocation) {
return processLocation.getShortName().substring(0, processLocation.getShortName().lastIndexOf("."));
}
return processLocation.getShortName();
case PROCESS_FILE:
return process.getProcessLocation() != null ? process.getProcessLocation().getShortName() : null;
case PROCESS_PATH:
return process.getProcessLocation() != null ? process.getProcessLocation().toString() : null;
case PROCESS_START:
return macroMap.containsKey(macro) ? macroMap.get(macro)
: DATE_FORMAT.get().format(new Date(process.getRootOperator().getStartTime()));
case Operator.STRING_EXPANSION_MACRO_TIME:
return DATE_FORMAT.get().format(new Date());
default:
return null;
}
}
return this.macroMap.get(macro);
}
/**
* Resolves the macro.
*
* <p>
* Resolves following predefined macros:
* </p>
* <ul>
* <li><b>process_name</b> with the name of the process</li>
* <li><b>process_file</b> with the file name of the process</li>
* <li><b>process_path</b> with the path to the process</li>
* <li><b>t</b> with the current system date and time</li>
* </ul>
* <p>
* Resolves following predefined macros if operator is non-null:
* </p>
* <ul>
* <li><b>n</b> or <b>operator_name</b> with the name of this operator</li>
* <li><b>c</b> with the class of this operator</li>
* <li><b>a</b> or <b>execution_count</b> with the number of times the operator was applied</li>
* <li><b>b</b> with the number of times the operator was applied plus one</li>
* </ul>
* <p>
* Resolves user defined macros.
* </p>
*
* @param macro
* the macro to resolve
* @param operator
* the operator to use for resolving, may be {@code null}
* @return the macro value
*/
public String getMacro(String macro, Operator operator) {
if (operator != null) {
String value = resolveUnshiftedOperatorMacros(macro, operator);
if (value != null) {
return value;
}
}
return getMacro(macro);
}
@Override
public String toString() {
return this.macroMap.toString();
}
/**
* This method replaces all Macros in a given String through their real values and returns a the
* String with replaced Macros. If the CompabililtyLevel of the RootOperator is lower than
* 6.0.3, undefined macros will be ignored.
*
* @param parameterValue
* the whole ParameterType value String
* @return the complete parameter value with replaced Macros
* @throws UndefinedParameterError
* this error will be thrown if the CompabilityLevel of the RootOperator is at least
* 6.0.3 and a macro is undefined
*/
public String resolveMacros(String parameterKey, String parameterValue) throws UndefinedMacroError {
int startIndex = parameterValue.indexOf(Operator.MACRO_STRING_START);
if (startIndex == -1) {
return parameterValue;
}
StringBuffer result = new StringBuffer();
while (startIndex >= 0) {
result.append(parameterValue.substring(0, startIndex));
int endIndex = parameterValue.indexOf(Operator.MACRO_STRING_END, startIndex + 2);
if (endIndex == -1) {
return parameterValue;
}
String macroString = parameterValue.substring(startIndex + 2, endIndex);
// check whether macroString is a predefined macro which will be resolved at String
// expansion
if (STRING_EXPANSION_MACRO_KEYS.contains(macroString) || LEGACY_STRING_EXPANSION_MACRO_KEYS
.contains(macroString.length() > 1 ? macroString.substring(0, 2) : macroString)) {
// skip macro because it will be replaced during the string expansion
result.append(Operator.MACRO_STRING_START + macroString + Operator.MACRO_STRING_END);
} else {
// resolve macro
String macroValue = this.getMacro(macroString);
if (macroValue != null) {
result.append(macroValue);
} else {
if (this.process.getRootOperator().getCompatibilityLevel().isAtLeast(THROW_ERROR_ON_UNDEFINED_MACRO)) {
throw new UndefinedMacroError(parameterKey, macroString);
} else {
result.append(Operator.MACRO_STRING_START + macroString + Operator.MACRO_STRING_END);
}
}
}
parameterValue = parameterValue.substring(endIndex + 1);
startIndex = parameterValue.indexOf(Operator.MACRO_STRING_START);
}
result.append(parameterValue);
return result.toString();
}
/**
* <p>
* Replaces following predefined macros:
* </p>
* <ul>
* <li><b>%{n}</b> or <b>%{operator_name}</b> with the name of this operator</li>
* <li><b>%{c}</b> with the class of this operator</li>
* <li><b>%{t}</b> with the current system date and time
* <li><b>%{a}</b> or <b>%{execution_count}</b> with the number of times the operator was
* applied</li>
* <li><b>%{b}</b> with the number of times the operator was applied plus one (a shortcut for
* %{p[1]})</li>
* <li><b>%{p[number]}</b> with the number of times the operator was applied plus number</li>
* <li><b>%{v[OperatorName.ValueName]}</b> with the value "ValueName" of the operator
* "OperatorName"</li>
* <li><b>%{%}</b> with %</li>
* </ul>
*
* @return The String with resolved predefined macros. Returns {@code null} in case provided
* parameter str is {@code null}.
*/
public String resolvePredefinedMacros(String str, Operator operator) throws UndefinedParameterError {
if (str == null) {
return null;
}
StringBuffer result = new StringBuffer();
int totalStart = 0;
int start = 0;
while ((start = str.indexOf(Operator.MACRO_STRING_START, totalStart)) >= 0) {
result.append(str.substring(totalStart, start));
int end = str.indexOf(Operator.MACRO_STRING_END, start);
if (end == -1) {
return str;
}
if (end >= start) {
String command = str.substring(start + 2, end);
String unshiftedOperatorMacroResult = resolveUnshiftedOperatorMacros(command, operator);
if (unshiftedOperatorMacroResult != null) {
result.append(unshiftedOperatorMacroResult);
} else if (command.startsWith(Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_SHIFTED
+ Operator.STRING_EXPANSION_MACRO_PARAMETER_START)) {
int openNumberIndex = command.indexOf(Operator.STRING_EXPANSION_MACRO_PARAMETER_START);
int closeNumberIndex = command.indexOf(Operator.STRING_EXPANSION_MACRO_PARAMETER_END, openNumberIndex);
if (closeNumberIndex < 0 || closeNumberIndex <= openNumberIndex + 1) {
throw new UndefinedMacroError(operator, "predefinedMacro_shiftedExecutionCounter_format", "");
}
String numberString = command.substring(openNumberIndex + 1, closeNumberIndex);
int number;
try {
number = Integer.parseInt(numberString);
} catch (NumberFormatException e) {
throw new UndefinedMacroError(operator, "946", numberString);
}
result.append(operator.getApplyCount() + number);
} else if (Operator.STRING_EXPANSION_MACRO_TIME.equals(command)) {
result.append(DATE_FORMAT.get().format(new Date()));
} else if (command.startsWith(
Operator.STRING_EXPANSION_MACRO_OPERATORVALUE + Operator.STRING_EXPANSION_MACRO_PARAMETER_START)) {
int openNumberIndex = command.indexOf(Operator.STRING_EXPANSION_MACRO_PARAMETER_START);
int closeNumberIndex = command.indexOf(Operator.STRING_EXPANSION_MACRO_PARAMETER_END, openNumberIndex);
if (closeNumberIndex < 0 || closeNumberIndex <= openNumberIndex + 1) {
throw new UndefinedMacroError(operator, "predefinedMacro_OperatorValue_format", "");
}
String operatorValueString = command.substring(openNumberIndex + 1, closeNumberIndex);
String[] operatorValuePair = operatorValueString.split("\\.");
if (operatorValuePair.length != 2) {
throw new UndefinedMacroError(operator, "predefinedMacro_OperatorValue_format", "");
}
Operator op = process.getOperator(operatorValuePair[0]);
if (op == null) {
throw new UndefinedMacroError(operator, "predefinedMacro_OperatorValue_wrongOperator",
operatorValuePair[0]);
}
Value value = op.getValue(operatorValuePair[1]);
if (value == null) {
throw new UndefinedMacroError(operator, "predefinedMacro_OperatorValue_noValue",
operatorValuePair[1]);
} else {
if (value.isNominal()) {
Object valueObject = value.getValue();
if (valueObject != null) {
result.append(valueObject.toString());
} else {
throw new UndefinedMacroError(operator, "predefinedMacro_OperatorValue_noValue",
operatorValuePair[1]);
}
} else {
double doubleValue = ((Double) value.getValue()).doubleValue();
if (!Double.isNaN(doubleValue)) {
result.append(Tools.formatIntegerIfPossible(doubleValue));
} else {
operator.logError("Value '" + operatorValuePair[1] + "' of the operator '"
+ operatorValuePair[0] + "' not found!");
}
}
}
} else if (Operator.STRING_EXPANSION_MACRO_PERCENT_SIGN.equals(command)) {
result.append('%');
} else {
result.append(command);
}
} else {
end = start + 2;
result.append(Operator.MACRO_STRING_START);
}
totalStart = end + 1;
}
result.append(str.substring(totalStart));
return result.toString();
}
/**
* <p>
* Resolves following predefined macros:
* </p>
* <ul>
* <li><b>n</b> or <b>operator_name</b> with the name of this operator</li>
* <li><b>c</b> with the class of this operator</li>
* <li><b>a</b> or <b>execution_count</b> with the number of times the operator was applied</li>
* <li><b>b</b> with the number of times the operator was applied plus one</li>
* </ul>
*
* Does not resolve p[].
*/
private String resolveUnshiftedOperatorMacros(String command, Operator operator) {
if (Operator.STRING_EXPANSION_MACRO_OPERATORNAME.equals(command)
|| Operator.STRING_EXPANSION_MACRO_OPERATORNAME_USER_FRIENDLY.equals(command)) {
return operator.getName();
} else if (Operator.STRING_EXPANSION_MACRO_OPERATORCLASS.equals(command)) {
return operator.getClass().getName();
} else if (Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES.equals(command)
|| Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_USER_FRIENDLY.equals(command)) {
return operator.getApplyCount() + "";
} else if (Operator.STRING_EXPANSION_MACRO_NUMBER_APPLIED_TIMES_PLUS_ONE.equals(command)) {
return operator.getApplyCount() + 1 + "";
} else {
return null;
}
}
}