/**
* 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.gui.properties;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.lang.StringEscapeUtils;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.AttributesImpl;
import com.rapidminer.Process;
import com.rapidminer.gui.MainFrame;
import com.rapidminer.gui.OperatorDocToHtmlConverter;
import com.rapidminer.gui.OperatorDocumentationBrowser;
import com.rapidminer.gui.actions.ToggleAction;
import com.rapidminer.gui.actions.ToggleExpertModeAction;
import com.rapidminer.gui.look.Colors;
import com.rapidminer.gui.processeditor.ProcessEditor;
import com.rapidminer.gui.properties.celleditors.value.PropertyValueCellEditor;
import com.rapidminer.gui.tools.ExtendedJScrollPane;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.ResourceDockKey;
import com.rapidminer.gui.tools.ResourceLabel;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.components.LinkLocalButton;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorVersion;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.Parameters;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.Observable;
import com.rapidminer.tools.Observer;
import com.rapidminer.tools.PlatformUtilities;
import com.vlsolutions.swing.docking.DockKey;
import com.vlsolutions.swing.docking.Dockable;
/**
* This panel displays parameters of an operator. It refreshes in either of these cases:
* <ul>
* <li>A new operator is selected.</li>
* <li>The {@link Parameters} of the current operator (which are observed) change in a way such that
* the parameter value differs from the one displayed by the editor. This should only happen if a
* parameter value is changed programmatically, e.g. by an operator.</li>
* <li>{@link #processUpdated(Process)} is called and {@link #getProperties()} returns a different
* list than the one returned during the last {@link #setupComponents()}.</li>
* <li>When changing to expert mode.</li>
* </ul>
*
* @author Simon Fischer, Tobias Malbrecht, Nils Woehler
*
*/
public class OperatorPropertyPanel extends PropertyPanel implements Dockable, ProcessEditor {
private static final long serialVersionUID = 6056794546696461864L;
private static final String TAG_PARAMETERS = "parameters";
private static final String TAG_PARAMETER = "parameter";
private static final String ATTRIBUTE_PARAMETER_KEY = "key";
/**
* {@link ExecutorService} used to execute XLST transformations of parameter help text.
*/
private static final ExecutorService PARAMETER_UPDATE_SERVICE = Executors.newCachedThreadPool();
private static final Icon OK_ICON = SwingTools.createIcon("16/check.png");
private static final Icon WARNING_ICON = SwingTools.createIcon("16/sign_warning.png");
private static final Border TOP_BORDER = BorderFactory.createMatteBorder(1, 0, 0, 0, Colors.PANEL_SEPARATOR);
private static final Border BOTH_BORDERS = BorderFactory.createMatteBorder(1, 0, 1, 0, Colors.PANEL_SEPARATOR);
private static final XMLInputFactory XML_STREAM_FACTORY = XMLInputFactory.newFactory();
public static final String PROPERTY_EDITOR_DOCK_KEY = "property_editor";
static {
XML_STREAM_FACTORY.setProperty(XMLInputFactory.IS_COALESCING, true);
}
private final DockKey DOCK_KEY = new ResourceDockKey(PROPERTY_EDITOR_DOCK_KEY);
{
DOCK_KEY.setDockGroup(MainFrame.DOCK_GROUP_ROOT);
}
private JPanel dockableComponent;
private final ToggleAction showHelpAction;
private final JLabel headerLabel = new JLabel("");
private final Font selectedFont = headerLabel.getFont().deriveFont(Font.BOLD);
private final Font unselectedFont = headerLabel.getFont();
private final LinkLocalButton changeCompatibility;
private final LinkLocalButton showAdvancedParameters;
private final LinkLocalButton hideAdvancedParameters;
private final Map<String, String> parameterDescriptionCache = new HashMap<>();
private Operator operator;
private final Observer<String> parameterObserver = new Observer<String>() {
@Override
public void update(Observable<String> observable, String key) {
PropertyValueCellEditor editor = getEditorForKey(key);
if (editor != null) {
ParameterType type = operator.getParameters().getParameterType(key);
String editorValue = type.toString(editor.getCellEditorValue());
String opValue = operator.getParameters().getParameterOrNull(key);
if (opValue != null && editorValue == null || opValue == null && editorValue != null || opValue != null
&& editorValue != null && !opValue.equals(editorValue)) {
editor.getTableCellEditorComponent(null, opValue, false, 0, 1);
}
} else {
setupComponents();
}
}
};
final transient ToggleAction TOGGLE_EXPERT_MODE_ACTION = new ToggleExpertModeAction();
private final JSpinner compatibilityLevelSpinner = new JSpinner(new CompatibilityLevelSpinnerModel());
private final ResourceLabel compatibilityLabel = new ResourceLabel("compatibility_level");
private final JPanel compatibilityPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
public OperatorPropertyPanel(final MainFrame mainFrame) {
super();
headerLabel.setHorizontalAlignment(SwingConstants.LEFT);
changeCompatibility = new LinkLocalButton(createCompatibilityAction(PlatformUtilities.getReleaseVersion()));
showAdvancedParameters = new LinkLocalButton(new ResourceAction(true, "parameters.show_advanced") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
TOGGLE_EXPERT_MODE_ACTION.actionPerformed(null);
}
});
hideAdvancedParameters = new LinkLocalButton(new ResourceAction(true, "parameters.hide_advanced") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
TOGGLE_EXPERT_MODE_ACTION.actionPerformed(null);
}
});
setupComponents();
compatibilityLevelSpinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
// compatibility level
OperatorVersion[] versionChanges = operator.getIncompatibleVersionChanges();
// sort to have an ascending order
Arrays.sort(versionChanges);
if (versionChanges.length > 0) {
OperatorVersion latestChange = versionChanges[versionChanges.length - 1];
if (latestChange.isAtLeast(operator.getCompatibilityLevel())) {
compatibilityLabel.setIcon(WARNING_ICON);
} else {
compatibilityLabel.setIcon(OK_ICON);
}
}
}
});
showHelpAction = new ToggleAction(true, "show_parameter_help") {
private static final long serialVersionUID = 1L;
@Override
public void actionToggled(ActionEvent e) {
setShowParameterHelp(isSelected());
mainFrame.getPropertyPanel().setupComponents();
}
};
}
@Override
public void processChanged(Process process) {}
@Override
public void processUpdated(Process process) {
setNameFor(operator);
// check if we have editors for the current parameters. If not, refresh.
int count = 0; // count hits. If we have to many, also refresh
List<ParameterType> properties = getProperties();
if (properties.size() != getNumberOfEditors()) {
setupComponents();
return;
}
for (ParameterType type : properties) {
if (hasEditorFor(type)) {
count++;
} else {
setupComponents();
return;
}
}
if (count != properties.size()) {
setupComponents();
}
}
@Override
public void setSelection(List<Operator> selection) {
final Operator operator = selection.isEmpty() ? null : selection.get(0);
if (operator == this.operator) {
return;
}
if (this.operator != null) {
this.operator.getParameters().removeObserver(parameterObserver);
}
this.operator = operator;
if (operator != null) {
this.operator.getParameters().addObserver(parameterObserver, true);
if (isShowParameterHelp()) {
PARAMETER_UPDATE_SERVICE.execute(new Runnable() {
@Override
public void run() {
parseParameterDescriptions(operator);
}
});
}
// compatibility level
OperatorVersion[] versionChanges = operator.getIncompatibleVersionChanges();
if (versionChanges.length == 0) {
// no incompatible versions exist
changeCompatibility.setVisible(false);
} else {
((CompatibilityLevelSpinnerModel) compatibilityLevelSpinner.getModel()).setOperator(operator);
changeCompatibility.setAction(createCompatibilityAction(operator.getCompatibilityLevel().getLongVersion()));
changeCompatibility.setVisible(true);
}
compatibilityLabel.setVisible(false);
compatibilityLevelSpinner.setVisible(false);
}
setNameFor(operator);
setupComponents();
}
@Override
public Component getComponent() {
if (dockableComponent == null) {
final JScrollPane scrollPane = new ExtendedJScrollPane(this);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.getViewport().addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (scrollPane.getVerticalScrollBar().isVisible()) {
scrollPane.setBorder(BOTH_BORDERS);
} else {
scrollPane.setBorder(TOP_BORDER);
}
}
});
dockableComponent = new JPanel(new BorderLayout());
JPanel headerPanel = new JPanel(new BorderLayout());
headerPanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 5));
headerPanel.add(headerLabel, BorderLayout.CENTER);
dockableComponent.add(headerPanel, BorderLayout.NORTH);
dockableComponent.add(scrollPane, BorderLayout.CENTER);
// compatibility level and warnings
JPanel advancedPanel = new JPanel();
advancedPanel.setLayout(new BoxLayout(advancedPanel, BoxLayout.PAGE_AXIS));
advancedPanel.add(showAdvancedParameters);
advancedPanel.add(hideAdvancedParameters);
compatibilityLabel.setLabelFor(compatibilityLevelSpinner);
compatibilityLevelSpinner.setPreferredSize(new Dimension(80, (int) compatibilityLevelSpinner.getPreferredSize()
.getHeight()));
compatibilityPanel.add(compatibilityLabel);
compatibilityPanel.add(compatibilityLevelSpinner);
JPanel compPanel = new JPanel();
compPanel.setLayout(new BoxLayout(compPanel, BoxLayout.PAGE_AXIS));
compPanel.add(changeCompatibility);
compPanel.add(compatibilityPanel);
JPanel combiPanel = new JPanel();
combiPanel.setLayout(new BoxLayout(combiPanel, BoxLayout.PAGE_AXIS));
combiPanel.add(advancedPanel);
combiPanel.add(compPanel);
dockableComponent.add(combiPanel, BorderLayout.SOUTH);
}
return dockableComponent;
}
@Override
public DockKey getDockKey() {
return DOCK_KEY;
}
public boolean isExpertMode() {
return TOGGLE_EXPERT_MODE_ACTION.isSelected();
}
public void setExpertMode(boolean isExpert) {
TOGGLE_EXPERT_MODE_ACTION.setSelected(isExpert);
}
@Override
public void setShowParameterHelp(boolean showHelp) {
super.setShowParameterHelp(showHelp);
showHelpAction.setSelected(showHelp);
}
@Override
protected Operator getOperator() {
return operator;
}
@Override
protected String getToolTipText(String key, String title, String description, String range, boolean isOptional) {
if (parameterDescriptionCache.containsKey(key)) {
description = parameterDescriptionCache.get(key);
}
return super.getToolTipText(key, title, description, range, isOptional);
}
@Override
protected String getValue(ParameterType type) {
return operator.getParameters().getParameterOrNull(type.getKey());
}
@Override
protected void setValue(Operator operator, ParameterType type, String value) {
if (value.length() == 0) {
value = null;
}
operator.setParameter(type.getKey(), value);
}
@Override
protected List<ParameterType> getProperties() {
List<ParameterType> visible = new LinkedList<ParameterType>();
int hidden = 0;
int advancedCount = 0;
if (operator != null) {
for (ParameterType type : operator.getParameters().getParameterTypes()) {
if (type.isHidden()) {
continue;
}
if (type.isExpert()) {
advancedCount++;
if (!isExpertMode()) {
hidden++;
continue;
}
}
visible.add(type);
}
}
if (hidden > 0) {
hideAdvancedParameters.setVisible(false);
showAdvancedParameters.setVisible(true);
showAdvancedParameters.setToolTipText(I18N.getGUIMessage("gui.action.parameters.show_advanced.tip", hidden));
} else {
showAdvancedParameters.setVisible(false);
if (advancedCount > 0) {
hideAdvancedParameters.setVisible(true);
hideAdvancedParameters.setToolTipText(I18N.getGUIMessage("gui.action.parameters.hide_advanced.tip",
advancedCount));
} else {
hideAdvancedParameters.setVisible(false);
}
}
return visible;
}
/**
* Starts a progress thread which parses the parameter descriptions for the provided operator ,
* cleans the {@link #parameterDescriptionCache}, and stores parsed descriptions in the
* {@link #parameterDescriptionCache}.
*/
private void parseParameterDescriptions(final Operator operator) {
parameterDescriptionCache.clear();
URL documentationURL = OperatorDocumentationBrowser.getDocResourcePath(operator);
if (documentationURL != null) {
try (InputStream documentationStream = documentationURL.openStream()) {
XMLStreamReader reader = XML_STREAM_FACTORY.createXMLStreamReader(documentationStream);
String parameterKey = null;
// The builder that stores the parameter description text
StringBuilder parameterTextBuilder = null;
boolean inParameters = false;
while (reader.hasNext()) {
switch (reader.next()) {
case XMLStreamReader.START_ELEMENT:
if (!inParameters && reader.getLocalName().equals(TAG_PARAMETERS)) {
inParameters = true;
} else {
AttributesImpl attributes = new AttributesImpl();
for (int i = 0; i < reader.getAttributeCount(); i++) {
attributes.addAttribute("", reader.getAttributeLocalName(i), reader.getAttributeName(i)
.toString(), reader.getAttributeType(i), reader.getAttributeValue(i));
}
// Check if no parameter was found
if (reader.getLocalName().equals(TAG_PARAMETER)) {
parameterKey = attributes.getValue(ATTRIBUTE_PARAMETER_KEY);
// In case a parameter key was found, create a new string
// builder
if (parameterKey != null) {
parameterTextBuilder = new StringBuilder();
}
}
if (parameterTextBuilder != null) {
appendParameterStartTag(reader.getLocalName(), attributes, parameterTextBuilder);
}
}
break;
case XMLStreamReader.END_ELEMENT:
// end parsing when end of parameters element is reached
if (reader.getLocalName().equals(TAG_PARAMETERS)) {
return;
}
if (parameterTextBuilder != null) {
// otherwise add element to description text
parameterTextBuilder.append("</");
parameterTextBuilder.append(reader.getLocalName());
parameterTextBuilder.append(">");
// Store description when parameter element ends
if (reader.getLocalName().equals(TAG_PARAMETER)) {
final String parameterDescription = parameterTextBuilder.toString();
final String key = parameterKey;
if (!parameterDescriptionCache.containsKey(parameterKey)) {
Source xmlSource = new StreamSource(new StringReader(parameterDescription));
try {
String desc = OperatorDocToHtmlConverter.applyXSLTTransformation(xmlSource);
parameterDescriptionCache.put(key, StringEscapeUtils.unescapeHtml(desc));
} catch (TransformerException e) {
// ignore
}
}
}
}
break;
case XMLStreamReader.CHARACTERS:
if (parameterTextBuilder != null) {
parameterTextBuilder.append(StringEscapeUtils.escapeHtml(reader.getText()));
}
break;
default:
// ignore other events
break;
}
}
} catch (IOException | XMLStreamException e) {
// ignore
}
}
}
private void appendParameterStartTag(String localName, Attributes attributes, StringBuilder parameterTextBuilder) {
parameterTextBuilder.append('<');
parameterTextBuilder.append(localName);
for (int i = 0; i < attributes.getLength(); i++) {
parameterTextBuilder.append(' ');
parameterTextBuilder.append(attributes.getLocalName(i));
parameterTextBuilder.append("=\"");
parameterTextBuilder.append(attributes.getValue(i));
parameterTextBuilder.append('"');
}
parameterTextBuilder.append(" >");
}
private void setNameFor(Operator operator) {
if (operator != null) {
headerLabel.setFont(selectedFont);
if (operator.getName().equals(operator.getOperatorDescription().getName())) {
headerLabel.setText(operator.getName());
} else {
headerLabel.setText(operator.getName() + " (" + operator.getOperatorDescription().getName() + ")");
}
headerLabel.setIcon(operator.getOperatorDescription().getSmallIcon());
} else {
headerLabel.setFont(unselectedFont);
headerLabel.setText("Select an operator to configure it.");
headerLabel.setIcon(null);
}
}
/**
* Creates the action to change compatibility mode.
*
* @param version
* @return
*/
private ResourceAction createCompatibilityAction(String version) {
String key = "parameters.change_compatibility_current";
// different action if old comp level
if (operator != null) {
OperatorVersion[] versionChanges = operator.getIncompatibleVersionChanges();
Arrays.sort(versionChanges);
if (versionChanges.length > 0) {
OperatorVersion latestChange = versionChanges[versionChanges.length - 1];
if (latestChange.isAtLeast(operator.getCompatibilityLevel())) {
key = "parameters.change_compatibility_old";
}
}
}
return new ResourceAction(true, key, version) {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
compatibilityLevelSpinner.setVisible(true);
compatibilityLabel.setVisible(true);
changeCompatibility.setVisible(false);
}
};
}
}