/**
* 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.parameter;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.logging.Level;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.rapidminer.MacroHandler;
import com.rapidminer.io.process.XMLTools;
import com.rapidminer.operator.Operator;
import com.rapidminer.parameter.conditions.ParameterCondition;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.Tools;
import com.rapidminer.tools.XMLException;
/**
* A ParameterType holds information about type, range, and default value of a parameter. Lists of
* ParameterTypes are provided by operators.
* <p>
* Sensitive information: <br/>
* If your parameter is expected to never contain sensitive data, e.g. the number of iterations for
* an algorithm, overwrite {@link #isSensitive()} and and return {@code false}. This method is used
* to determine how parameters are handled in certain places, e.g. if their value is replaced before
* uploading the process for operator recommendations. The default implementation always returns
* {@code true}.
* </p>
*
* @author Ingo Mierswa, Simon Fischer
* @see com.rapidminer.operator.Operator#getParameterTypes()
*/
public abstract class ParameterType implements Comparable<ParameterType>, Serializable {
private static final long serialVersionUID = 5296461242851710130L;
public static final String ELEMENT_PARAMETER_TYPE = "ParameterType";
private static final String ELEMENT_DESCRIPTION = "Description";
private static final String ELEMENT_CONDITIONS = "Conditions";
private static final String ATTRIBUTE_EXPERT = "is-expert";
private static final String ATTRIBUTE_DEPRECATED = "is-deprecated";
private static final String ATTRIBUTE_HIDDEN = "is-hidden";
private static final String ATTRIBUTE_OPTIONAL = "is-optional";
private static final String ATTRIBUTE_SHOW_RANGE = "is-range-shown";
private static final String ATTRIBUTE_KEY = "key";
private static final String ATTRIBUTE_CONDITION_CLASS = "condition-class";
private static final String ATTRIBUTE_CLASS = "class";
/** The key of this parameter. */
private String key;
/** The documentation. Used as tooltip text... */
private String description;
/**
* Indicates if this is a parameter only viewable in expert mode. Mandatory parameters are
* always viewable. The default value is true.
*/
private boolean expert = true;
/**
* Indicates if this parameter is hidden and is not shown in the GUI. May be used in conjunction
* with a configuration wizard which lets the user configure the parameter.
*/
private boolean isHidden = false;
/** Indicates if the range should be displayed. */
private boolean showRange = true;
/**
* Indicates if this parameter is optional unless a dependency condition made it mandatory.
*/
private boolean isOptional = true;
/**
* Indicates that this parameter is deprecated and remains only for compatibility reasons during
* loading of older processes. It should neither be shown nor documented.
*/
private boolean isDeprecated = false;
/**
* This collection assembles all conditions to be met to show this parameter within the gui.
*/
private final Collection<ParameterCondition> conditions = new LinkedList<>();
/**
* This is the inversed constructor to {@link #getDefinitionAsXML(Document)}. It will reload all
* settings of this {@link ParameterType} from the given XML Element. Subclasses MUST implement
* this constructor, since it is called by reflection.
*
* @throws XMLException
*/
public ParameterType(Element element) throws XMLException {
loadDefinitionFromXML(element);
}
/** Creates a new ParameterType. */
public ParameterType(String key, String description) {
this.key = key;
this.description = description;
}
public abstract Element getXML(String key, String value, boolean hideDefault, Document doc);
/** Returns a human readable description of the range. */
public abstract String getRange();
/** Returns a value that can be used if the parameter is not set. */
public abstract Object getDefaultValue();
/**
* Returns the correct string representation of the default value. If the default is undefined,
* it returns null.
*/
public String getDefaultValueAsString() {
return toString(getDefaultValue());
}
/** Sets the default value. */
public abstract void setDefaultValue(Object defaultValue);
/**
* Returns true if the values of this parameter type are numerical, i.e. might be parsed by
* {@link Double#parseDouble(String)}. Otherwise false should be returned. This method might be
* used by parameter logging operators.
*/
public abstract boolean isNumerical();
/**
* Writes an xml representation of the given key-value pair.
*
* @deprecated Use the DOM version of this method. At the moment, we cannot delete it, because
* {@link Parameters#equals(Object)} and {@link Parameters#hashCode()} rely on it.
*/
@Deprecated
public abstract String getXML(String indent, String key, String value, boolean hideDefault);
public boolean showRange() {
return showRange;
}
public void setShowRange(boolean showRange) {
this.showRange = showRange;
}
/**
* This method will be invoked by the Parameters after a parameter was set. The default
* implementation is empty but subclasses might override this method, e.g. for a decryption of
* passwords.
*/
public String transformNewValue(String value) {
return value;
}
/**
* Returns true if this parameter can only be seen in expert mode. The default implementation
* returns true if the parameter is optional. It is ensured that an non-optional parameter is
* never expert!
*/
public boolean isExpert() {
return expert && isOptional;
}
/**
* Sets if this parameter can be seen in expert mode (true) or beginner mode (false).
*
*/
public void setExpert(boolean expert) {
this.expert = expert;
}
/**
* Returns true if this parameter is hidden or not all dependency conditions are fulfilled. Then
* the parameter will not be shown in the GUI. The default implementation returns true which
* should be the normal case.
*
* Please note that this method cannot be accessed during getParameterTypes() method
* invocations, because it relies on getting the Parameters object, which is then not created.
*/
public boolean isHidden() {
boolean conditionsMet = true;
for (ParameterCondition condition : conditions) {
conditionsMet &= condition.dependencyMet();
}
return isDeprecated || isHidden || !conditionsMet;
}
public Collection<ParameterCondition> getConditions() {
return Collections.unmodifiableCollection(conditions);
}
/**
* Sets if this parameter is hidden (value true) and will not be shown in the GUI.
*/
public void setHidden(boolean hidden) {
this.isHidden = hidden;
}
/**
* This returns whether this parameter is deprecated.
*/
public boolean isDeprecated() {
return this.isDeprecated;
}
/**
* This method indicates that this parameter is deprecated and isn't used anymore beside from
* loading old process files.
*/
public void setDeprecated() {
this.isDeprecated = true;
}
/**
* This sets if the parameter is optional or must be entered. If it is not optional, it may not
* be an expert parameter and the expert status will be ignored!
*/
public final void setOptional(boolean isOptional) {
this.isOptional = isOptional;
}
/** Registers the given dependency condition. */
public void registerDependencyCondition(ParameterCondition condition) {
this.conditions.add(condition);
}
public Collection<ParameterCondition> getDependencyConditions() {
return this.conditions;
}
/**
* Returns true if this parameter is optional. The default implementation returns true. Please
* note that this method cannot be accessed during {@link Operator#getParameterTypes()} method
* invocations, because it relies on getting the Parameters object, which is then not created.
*
*/
public final boolean isOptional() {
if (isOptional) {
// if parameter is optional per default: check conditions
boolean becomeMandatory = false;
for (ParameterCondition condition : conditions) {
if (condition.dependencyMet()) {
becomeMandatory |= condition.becomeMandatory();
} else {
return true;
}
}
return !becomeMandatory;
}
// otherwise it is mandatory even without dependency
return false;
}
/**
* Checks whether the parameter is configured to be optional without looking at the parameter
* conditions. This method can be invoked during {@link Operator#getParameterTypes()} method
* invocations as it does not check parameter conditions. It does not reflect the actual state
* though as parameter conditions might change an optional parameter to become mandatory.
*
* @return whether the parameter is optional without checking the parameter conditions
*/
public final boolean isOptionalWithoutConditions() {
return isOptional;
}
/** Sets the key. */
public void setKey(String key) {
this.key = key;
}
/** Returns the key. */
public String getKey() {
return key;
}
/** Returns a short description. */
public String getDescription() {
return description;
}
/** Sets the short description. */
public void setDescription(String description) {
this.description = description;
}
/**
* States whether a given parameter type implementation may contain sensitive information or
* not. Sensitive information are obvious things like passwords, files, OAuth tokens, or
* database connections. However less obvious things like SQL queries can also contain sensitive
* information.
*
* @return always {@code true}
*/
public boolean isSensitive() {
return true;
}
/**
* This method gives a hook for the parameter type to react on a renaming of an operator. It
* must return the correctly modified String value. The default implementation does nothing.
*/
public String notifyOperatorRenaming(String oldOperatorName, String newOperatorName, String parameterValue) {
return parameterValue;
}
/** Returns a string representation of this value. */
public String toString(Object value) {
if (value == null) {
return "";
} else {
return value.toString();
}
}
public String toXMLString(Object value) {
return Tools.escapeXML(toString(value));
}
@Override
public String toString() {
return key + " (" + description + ")";
}
/**
* Can be called in order to report an illegal parameter value which is encountered during
* <tt>checkValue()</tt>.
*/
public void illegalValue(Object illegal, Object corrected) {
LogService.getRoot().log(Level.WARNING, "com.rapidminer.parameter.ParameterType.illegal_value_for_parameter",
new Object[] { illegal, key, corrected.toString() });
}
@Override
public int compareTo(ParameterType o) {
/* ParameterTypes are compared by key. */
return this.key.compareTo(o.key);
}
/**
* This method operates on the internal string representation of parameter values and replaces
* macro expressions of the form %{macroName}.
*/
public abstract String substituteMacros(String parameterValue, MacroHandler mh) throws UndefinedParameterError;
/**
* This method replaces predefined macro values. It is called right after
* {@link #substituteMacros(String, MacroHandler)}.
* <p>
* Override this method in case a custom parameter type should not replace predefined macros on
* parameter value fetching.
*
* @param parameterValue
* the parameter value which is the result of
* {@link #substituteMacros(String, MacroHandler)}
* @param operator
* the calling operator. Must not be <code>null</code>.
* @return the parameter string with replaced predefined macros
* @throws UndefinedParameterError
* in case a predefined macro is malformed
*/
public String substitutePredefinedMacros(String parameterValue, Operator operator) throws UndefinedParameterError {
return operator.getProcess().getMacroHandler().resolvePredefinedMacros(parameterValue, operator);
}
/**
* This method will write the definition of this {@link ParameterType} into the {@link Element}
* that is returned. This XML representation can be used to load the {@link ParameterType} later
* on again using the static {@link #createType(Element)} method.
*/
public final Element getDefinitionAsXML(Document document) {
Element typeElement = document.createElement(ELEMENT_PARAMETER_TYPE);
// class name for reconstruction
typeElement.setAttribute(ATTRIBUTE_CLASS, this.getClass().getCanonicalName());
// simple properties
typeElement.setAttribute(ATTRIBUTE_KEY, key);
typeElement.setAttribute(ATTRIBUTE_EXPERT, expert + "");
typeElement.setAttribute(ATTRIBUTE_HIDDEN, isHidden + "");
typeElement.setAttribute(ATTRIBUTE_DEPRECATED, isDeprecated + "");
typeElement.setAttribute(ATTRIBUTE_SHOW_RANGE, showRange + "");
typeElement.setAttribute(ATTRIBUTE_OPTIONAL, isOptional + "");
// description
XMLTools.addTag(typeElement, ELEMENT_DESCRIPTION, description);
// conditions
Element conditionsElement = XMLTools.addTag(typeElement, ELEMENT_CONDITIONS);
for (ParameterCondition condition : conditions) {
Element conditionElement = condition.getDefinitionAsXML(document);
// setting class name for reconstruction
conditionElement.setAttribute(ATTRIBUTE_CONDITION_CLASS, condition.getClass().getName());
conditionsElement.appendChild(conditionElement);
}
writeDefinitionToXML(typeElement);
return typeElement;
}
/**
* Subclasses must store all their properties inside the typeElement and must be able to reload
* it from their using the constructor (Operator operator, Element element). This constructor is
* called via reflection. This method should be abstract, but in order to keep the class
* compatible with existing extensions, this only throws an unsupported exception.
*/
protected void writeDefinitionToXML(Element typeElement) {
throw new UnsupportedOperationException("The Subclass " + this.getClass().getCanonicalName()
+ " must override the method getDefinitionAsXML(Element) of the super type "
+ ParameterType.class.getCanonicalName());
}
private void loadDefinitionFromXML(Element typeElement) throws XMLException {
// simple properties
key = typeElement.getAttribute(ATTRIBUTE_KEY);
expert = Boolean.parseBoolean(typeElement.getAttribute(ATTRIBUTE_EXPERT));
isHidden = Boolean.parseBoolean(typeElement.getAttribute(ATTRIBUTE_HIDDEN));
isDeprecated = Boolean.parseBoolean(typeElement.getAttribute(ATTRIBUTE_DEPRECATED));
isOptional = Boolean.parseBoolean(typeElement.getAttribute(ATTRIBUTE_OPTIONAL));
showRange = Boolean.parseBoolean(typeElement.getAttribute(ATTRIBUTE_SHOW_RANGE));
// description
description = XMLTools.getTagContents(typeElement, ELEMENT_DESCRIPTION);
// conditions
try {
Collection<Element> conditionElements = XMLTools.getChildElements(XMLTools.getChildElement(typeElement,
ELEMENT_CONDITIONS, true));
for (Element conditionElement : conditionElements) {
String className = conditionElement.getAttribute(ATTRIBUTE_CONDITION_CLASS);
Class<?> conditionClass = Class.forName(className);
Constructor<?> constructor = conditionClass.getConstructor(Element.class);
conditions.add((ParameterCondition) constructor.newInstance(conditionElement));
}
} catch (ClassNotFoundException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CONDITION_CLASS, e);
} catch (IllegalArgumentException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CONDITION_CLASS, e);
} catch (InstantiationException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CONDITION_CLASS, e);
} catch (IllegalAccessException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CONDITION_CLASS, e);
} catch (InvocationTargetException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CONDITION_CLASS, e);
} catch (SecurityException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CONDITION_CLASS, e);
} catch (NoSuchMethodException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CONDITION_CLASS, e);
}
}
/**
* This creates the ParameterType defined by the given element for the given operator.
*
* @throws XMLException
*/
public static ParameterType createType(Element element) throws XMLException {
String className = element.getAttribute(ATTRIBUTE_CLASS);
try {
Class<?> typeClass = Class.forName(className);
Constructor<?> constructor = typeClass.getConstructor(Element.class);
Object type = constructor.newInstance(element);
return (ParameterType) type;
} catch (ClassNotFoundException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CLASS, e);
} catch (SecurityException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CLASS, e);
} catch (NoSuchMethodException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CLASS, e);
} catch (IllegalArgumentException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CLASS, e);
} catch (InstantiationException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CLASS, e);
} catch (IllegalAccessException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CLASS, e);
} catch (InvocationTargetException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CLASS, e);
} catch (ClassCastException e) {
throw new XMLException("Illegal value for attribute " + ATTRIBUTE_CLASS, e);
}
}
}