[PARTIALLY SOLVED] Dynamically updating a parameter's list of values

Unknown
edited November 5 in Community Q&A
People have hit a similar problem at least a couple of times before: see http://rapid-i.com/rapidforum/index.php/topic,5020.0.html and, more recently, http://rapid-i.com/rapidforum/index.php/topic,2992.0.html

The situation arises sometimes, when writing a new RapidMiner operator, where one wishes to change one parameter's list of possible values (using ParameterTypeCategory or ParameterTypeStringCategory) as a result of changing another parameter's value.  Typically, one connects the operator to some data source (e.g. a database), and this source describes the possible values that another parameter can take (e.g. a list of database table names).

The problem, of course, is that neither ParameterTypeCategory nor ParameterTypeStringCategory offers a way to change their set of possible values, stored as "private String[] categories".  Redefining the operator's set of parameters isn't an option either, because Parameters' keyToValueMap and keyToTypeMap are both private and final.

The standard recourse recommended in these hallowed halls has been to create multiple hidden list-parameters and activate the right one depending on the controlling parameter's value.  This can be tedious if not downright impossible when the number of alternatives is large.

A first step to solve this is to create a descendant of ParameterTypeStringCategory (a similar class can be descended from ParameterTypeCategory) which I called ParameterTypeMutableStringCategory:

package com.rapidminer.parameter;

import org.w3c.dom.Element;

import com.rapidminer.io.process.XMLTools;
import com.rapidminer.tools.XMLException;

public class ParameterTypeMutableStringCategory extends ParameterTypeStringCategory {

  private static final long serialVersionUID = 1L;

  // Shadows String[] categories from ParameterTypeStringCategory
  private String[] mutablecategories = new String[0];

  /**
   * Constructor.
   * @param element An XML element describing the parameter type
   * @throws XMLException
   */
  public ParameterTypeMutableStringCategory(Element element)
        throws XMLException
  {
     super(element);
     mutablecategories = super.getValues();
  }

  /**
   * Alternate constructor.
   * @param key         The parameter type's name
   * @param description The parameter type's description
   * @param categories  The array of values the parameter type may take
   */
  public ParameterTypeMutableStringCategory(String key, String description, String[] categories) {
     this(key, description, categories, null);
  }

  /**
   * Alternate constructor.
   * @param key          The parameter type's name
   * @param description  The parameter type's description
   * @param categories   The array of values the parameter type may take
   * @param defaultValue The parameter type's default value (among the categories)
   */
  public ParameterTypeMutableStringCategory(String key, String description, String[] categories, String defaultValue) {
     this(key, description, categories, defaultValue, true);
  }

  /**
   * Alternate constructor.
   * @param key          The parameter type's name
   * @param description  The parameter type's description
   * @param categories   The array of values the parameter type may take
   * @param defaultValue The parameter type's default value (among the categories)
   * @param editable     Whether the categories may change or not
   */
  public ParameterTypeMutableStringCategory(String key, String description, String[] categories, String defaultValue, boolean editable) {
     super(key, description, categories, defaultValue, editable);
     this.mutablecategories = super.getValues();
  }

  @Override //ParameterTypeStringCategory, ParameterType
  public String getRange() {
     StringBuffer values = new StringBuffer();
     for (int i = 0; i < mutablecategories.length; i++) {
        if (i > 0) values.append(", ");
        values.append(mutablecategories);
     }
     if (getDefaultValue() != null) values.append("; default: '" + getDefaultValue() + "'");
     return values.toString();
  }

  @Override //ParameterTypeStringCategory, ParameterType
  protected void writeDefinitionToXML(Element typeElement) {
     super.writeDefinitionToXML(typeElement);
     // typeElement has an optional "default" child and a single mandatory
     //"Values" child; the latter has zero or more "Value" children.
     //private static final String ELEMENT_VALUES = "Values";
     Element valuesElement;
     try {
        valuesElement = XMLTools.getChildElement(typeElement, "Values", true);
        XMLTools.deleteTagContents(valuesElement, "Value");
     } catch (XMLException e) {
        valuesElement = XMLTools.addTag(typeElement, "Values");
     }
     for (String category : mutablecategories) {
        //private static final String ELEMENT_VALUE = "Value";
        XMLTools.addTag(valuesElement, "Value", category);
     }
  }

  @Override //ParameterTypeStringCategory
  public String[] getValues() {
     return mutablecategories;
  }

  /**
   * The whole point of this class is this method.
   * If the default value is not in the newcategories, the default is nulled.
   * @param newcategories A new String[] that replaces the previous one
   */
  public void setValues(String[] newcategories) {
     mutablecategories = newcategories;
     for (String category : mutablecategories) {
        if (category.equals(getDefaultValue())) return;
     }
     setDefaultValue(null);
  }
}
Note how I was forced to hard-code the values of two private static final String fields of ParameterTypeStringCategory (ELEMENT_VALUES and ELEMENT_VALUE).  These should have been made protected instead.

The next step is in the operator's getParameterTypes: when creating a new (initially optional) ParameterTypeMutableStringCategory for inclusion in the List<ParameterType>, I call registerDependencyCondition with a new ParameterCondition with an overridden isConditionFullfilled.  In this method, I can check the controlling parameter's value (e.g. the database connection URL) and, if the condition is properly fulfilled, fetch the new set of categories I want to put in the controlled parameter.  This is achieved with a line like:

((ParameterTypeMutableStringCategory) parameterHandler.getParameters().getParameterType(conditionParameter)).setValues(the_new_String_array);
It seems an alternate approach would be to addObserver() to the operator's Parameters, but I get the impression, wandering around the code, that observers get called everywhere within the user's process every time a parameter changes somewhere.  This is survivable if one makes sure the observer fails fast, but it seems gross overkill compared to the use cases outlined here.

Am I right in my interpretation of the purpose and functionality of observers?

Answers

  • Nils_Woehler
    Nils_Woehler New Altair Community Member
    Hi Urhixidur,

    first of all you are right. 'static final' fields should always be at least protected. I've changed it and made them protected.
    Regarding your second question: Yes, an observer will be called every time a parameter has been changed by the user.
    Last but not least I've created an issue in our issue tracker to discuss your ParameterType proposal. But I've to admit at the moment this won't have a high priority for us right now.

    Best,
    Nils