/* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /* * GenericObjectNode.java * Copyright (C) 2006 Robert Jung * */ package weka.gui.ensembleLibraryEditor.tree; import weka.classifiers.Classifier; import weka.gui.GenericObjectEditor; import weka.gui.ensembleLibraryEditor.AddModelsPanel; import javax.swing.*; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import java.awt.*; import java.beans.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Vector; /** * This class is responsible for allowing users to choose an object that * was provided with a GenericObjectEditor. Just about every one of these * Objects is a Weka Classifier. There are two important things that these * nodes are responsible for beyond the other parameter node types. First, * they must discover all of the parameters that need to be added in the * model as child nodes. This is done through a loop of introspection that * was copied and adapted from the weka.gui.PropertySheetPanel class. * Second, this class is also responsible for discovering all possible * combinations of GenericObject parameters that are stored in its child * nodes. This is accomplished by first discovering all of the child node * parameters in the getValues method and then finding all combinations of * these values with the combinAllValues method. * * @author Robert Jung (mrbobjung@gmail.com) * @version $Revision: 1.1 $ */ public class GenericObjectNode extends DefaultMutableTreeNode implements PropertyChangeListener { /** for serialization */ private static final long serialVersionUID = 688096727663132485L; //The following 8 arrays hold the accumulated information about the //Classifier parameters that we discover through introspection. This //is very similar to the approach within PropertySheetPanel. /** Holds properties of the target */ private PropertyDescriptor m_Properties[]; /** this tracks which indexes of the m_Properties */ private Vector m_UsedPropertyIndexes; /** Holds the methods of the target */ private MethodDescriptor m_Methods[]; /** Holds property editors of the object */ private PropertyEditor m_Editors[]; /** Holds current object values for each property */ private Object m_Values[]; /** The labels for each property */ private String m_Names[]; /** The tool tip text for each property */ private String m_TipTexts[]; /** StringBuffer containing help text for the object being edited */ private StringBuffer m_HelpText; /** the GenericObjectEditor that was supplied for this node */ private GenericObjectEditor m_GenericObjectEditor; /** this Vector stores all of the possible combinations of parameters * that it obtains from its child nodes. These combinations are * created by the recursive combineAllValues method*/ private Vector m_WorkingSetCombinations; /** the tip text for our node editor to display */ private String m_ToolTipText; /** a reference to the tree model is necessary to be able to add and * remove nodes in the tree */ private DefaultTreeModel m_TreeModel; /** this is a reference to the Tree object that this node is * contained within. Its required for this node to be able to * add/remove nodes from the JTree*/ private JTree m_Tree; /** This is a reference to the parent panel of the JTree so that we can * supply it as the required argument when supplying warning JDialog * messages*/ private final AddModelsPanel m_ParentPanel; /** * The constructor initialiazes the member variables of this node, * Note that the "value" of this generic object is stored as the treeNode * user object. * * @param panel the reference to the parent panel for calls to JDialog * @param value the value stored at this tree node * @param genericObjectEditor the GenericObjectEditor for this object * @param toolTipText the tipText to be displayed for this object */ public GenericObjectNode(AddModelsPanel panel, Object value, GenericObjectEditor genericObjectEditor, String toolTipText) { super(value); //setObject(value); m_ParentPanel = panel; this.m_GenericObjectEditor = genericObjectEditor; this.m_ToolTipText = toolTipText; } /** * It seems kind of dumb that the reference to the tree model is passed in * seperately - but know that this is actually necessary. There is a really * weird chicken before the egg problem. You cannot create a TreeModel without * giving it its root node. However, the nodes in your tree can't update the * structure of the tree unless they have a reference to the TreeModel. So in * the end this was the only compromise that I could get to work well * * @param tree the tree to use */ public void setTree(JTree tree) { this.m_Tree = tree; this.m_TreeModel = (DefaultTreeModel) m_Tree.getModel(); } /** * returns the current tree * * @return the current tree */ public JTree getTree() { return m_Tree; } /** * A getter for the GenericObjectEditor for this node * * @return the editor */ public GenericObjectEditor getEditor() { return m_GenericObjectEditor; } /** * getter for the tooltip text * * @return tooltip text */ public StringBuffer getHelpText() { return m_HelpText; } /** * getter for the tooltip text * * @return tooltip text */ public String getToolTipText() { return m_ToolTipText; } /** * getter for this node's object * * @return the node's object */ public Object getObject() { return getUserObject(); } /** * setter for this nodes object * * @param newValue sets the new object */ public void setObject(Object newValue) { setUserObject(newValue); } /** * this is a simple filter for the setUserObject method. We basically * don't want null values to be passed in. * * @param o the object to set */ public void setUserObject(Object o) { if (o != null) super.setUserObject(o); } /** * getter for the parent panel * * @return the parent panel */ public JPanel getParentPanel() { return m_ParentPanel; } /** * returns always null * * @return always null */ public String toString() { return null; //return getClass().getName() + "[" + getUserObject().toString() + "]"; } /** * This implements the PropertyChangeListener for this node that gets * registered with its Editor. All we really have to do is change the * Object value stored internally at this node when its editor says the * value changed. * * @param evt the event */ public void propertyChange(PropertyChangeEvent evt) { Object newValue = ((GenericObjectEditor) evt.getSource()).getValue(); if (!newValue.getClass().equals(getObject().getClass())) { if (m_TreeModel.getRoot() == this) { try { m_ParentPanel.buildClassifierTree((Classifier) newValue .getClass().newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } m_ParentPanel.update(m_ParentPanel.getGraphics()); m_ParentPanel.repaint(); //System.out.println("Changed root"); } else { setObject(newValue); updateTree(); updateTree(); m_TreeModel.nodeChanged(this); } } } /** * This method uses introspection to programatically discover all of * the parameters for this generic object. For each one of them it * uses the TreeModel reference to create a new subtree to represent * that parameter and its value ranges. Note that all of these nodes * are PropertyNodes which themselves hold the logic of figuring out * what type of parameter it is they are representing and thus what * type of subtree to build. * <p/> * We need to be careful because this was molded from the code inside of * the PropertySheetPanel class. Which means that we are wide open * to copy/paste problems. In the future, when that code changes to * adapt to other changes in Weka then this could easily become broken. */ public void updateTree() { int childCount = m_TreeModel.getChildCount(this); for (int i = 0; i < childCount; i++) { DefaultMutableTreeNode child = (DefaultMutableTreeNode) m_TreeModel.getChild(this, 0); m_TreeModel.removeNodeFromParent(child); } //removeAllChildren(); Object classifier = this.getUserObject(); try { BeanInfo bi = Introspector.getBeanInfo(classifier.getClass()); m_Properties = bi.getPropertyDescriptors(); m_Methods = bi.getMethodDescriptors(); } catch (IntrospectionException ex) { System.err.println("PropertySheet: Couldn't introspect"); return; } // Look for a globalInfo method that returns a string // describing the target for (int i = 0; i < m_Methods.length; i++) { String name = m_Methods[i].getDisplayName(); Method meth = m_Methods[i].getMethod(); if (name.equals("globalInfo")) { if (meth.getReturnType().equals(String.class)) { try { Object args[] = {}; String globalInfo = (String) (meth.invoke(getObject(), args)); String summary = globalInfo; int ci = globalInfo.indexOf('.'); if (ci != -1) { summary = globalInfo.substring(0, ci + 1); } final String className = getObject().getClass().getName(); m_HelpText = new StringBuffer("NAME\n"); m_HelpText.append(className).append("\n\n"); m_HelpText.append("SYNOPSIS\n").append(globalInfo).append("\n\n"); } catch (Exception ex) { // ignored } } } } m_UsedPropertyIndexes = new Vector(); m_Editors = new PropertyEditor[m_Properties.length]; m_Values = new Object[m_Properties.length]; m_Names = new String[m_Properties.length]; m_TipTexts = new String[m_Properties.length]; boolean firstTip = true; for (int i = 0; i < m_Properties.length; i++) { // Don't display hidden or expert properties. if (m_Properties[i].isHidden() || m_Properties[i].isExpert()) { continue; } m_Names[i] = m_Properties[i].getDisplayName(); Class type = m_Properties[i].getPropertyType(); Method getter = m_Properties[i].getReadMethod(); Method setter = m_Properties[i].getWriteMethod(); // Only display read/write properties. if (getter == null || setter == null) { continue; } try { Object args[] = {}; Object value = getter.invoke(classifier, args); m_Values[i] = value; PropertyEditor editor = null; Class pec = m_Properties[i].getPropertyEditorClass(); if (pec != null) { try { editor = (PropertyEditor) pec.newInstance(); } catch (Exception ex) { // Drop through. } } if (editor == null) { editor = PropertyEditorManager.findEditor(type); } m_Editors[i] = editor; // If we can't edit this component, skip it. if (editor == null) { continue; } if (editor instanceof GenericObjectEditor) { ((GenericObjectEditor) editor).setClassType(type); } // Don't try to set null values: if (value == null) { continue; } editor.setValue(value); // now look for a TipText method for this property String tipName = m_Names[i] + "TipText"; for (int j = 0; j < m_Methods.length; j++) { String mname = m_Methods[j].getDisplayName(); Method meth = m_Methods[j].getMethod(); if (mname.equals(tipName)) { if (meth.getReturnType().equals(String.class)) { try { String tempTip = (String) (meth.invoke( classifier, args)); int ci = tempTip.indexOf('.'); if (ci < 0) { m_TipTexts[i] = tempTip; } else { m_TipTexts[i] = tempTip.substring(0, ci); } if (m_HelpText != null) { if (firstTip) { m_HelpText.append("OPTIONS\n"); firstTip = false; } m_HelpText.append(m_Names[i]).append(" -- "); m_HelpText.append(tempTip).append("\n\n"); } } catch (Exception ex) { } break; } } } //Here we update the usedPropertyIndexes variable so that //later on we will know which ones to look at. m_UsedPropertyIndexes.add(new Integer(i)); int currentCount = m_TreeModel.getChildCount(this); //Now we make a child node and add it to the tree underneath //this one PropertyNode newNode = new PropertyNode(m_Tree, m_ParentPanel, m_Names[i], m_TipTexts[i], m_Values[i], m_Editors[i]); m_TreeModel.insertNodeInto(newNode, this, currentCount); } catch (InvocationTargetException ex) { System.err.println("Skipping property " + m_Names[i] + " ; exception on target: " + ex.getTargetException()); ex.getTargetException().printStackTrace(); continue; } catch (Exception ex) { System.err.println("Skipping property " + m_Names[i] + " ; exception: " + ex); ex.printStackTrace(); continue; } } //Finally we tell the TreeModel to update itself so the changes //will be visible m_TreeModel.nodeStructureChanged(this); } /** * This method iterates over all of the child nodes of this * GenericObjectNode and requests the verious sets of values that the * user has presumably specified. Once these sets of values are * * @return a Vector consisting of all parameter combinations */ public Vector getValues() { Vector valuesVector = new Vector(); int childCount = m_TreeModel.getChildCount(this); //poll all child nodes for their values. for (int i = 0; i < childCount; i++) { PropertyNode currentChild = (PropertyNode) m_TreeModel.getChild( this, i); Vector v = currentChild.getAllValues(); valuesVector.add(v); } //Need to initialize the working set of paramter combinations m_WorkingSetCombinations = new Vector(); //obtain all combinations of the paremeters combineAllValues(new Vector(), valuesVector); /* //nice for initially debugging this - and there was a WHOLE lot of //that going on till this crazy idea finally worked. for (int i = 0; i < m_WorkingSetCombinations.size(); i++) { System.out.print("Combo "+i+": "); Vector current = (Vector)m_WorkingSetCombinations.get(i); for (int j = 0; j < current.size(); j++) { System.out.print(current.get(j)+"\t"); } System.out.print("\n"); } */ //Now the real work begins. Here we need to translate all of the values //received from the editors back into the actual class types that the //Weka classifiers will understand. for example, String values for //enumerated values need to be turned back into the SelectedTag objects //that classifiers understand. //This vector will hold all of the actual generic objects that are being //instantiated Vector newGenericObjects = new Vector(); for (int i = 0; i < m_WorkingSetCombinations.size(); i++) { Vector current = (Vector) m_WorkingSetCombinations.get(i); //create a new copy of this class. We will use this copy to test whether //the current set of parameters is valid. Object o = this.getUserObject(); Class c = o.getClass(); Object copy = null; try { copy = c.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } for (int j = 0; j < current.size(); j++) { Object[] args = new Object[1]; int index = ((Integer) m_UsedPropertyIndexes.get(j)).intValue(); PropertyDescriptor property = (PropertyDescriptor) m_Properties[index]; Method setter = property.getWriteMethod(); Class[] params = setter.getParameterTypes(); Object currentVal = current.get(j); //System.out.println(currentVal.getClass().toString()); //we gotta turn strings back into booleans if (params.length == 1 && params[0].toString().equals("boolean") && currentVal.getClass().toString().equals( "class java.lang.String")) { currentVal = new Boolean((String) current.get(j)); } //we gotta turn strings back into "Tags" if (params.length == 1 && params[0].toString().equals( "class weka.core.SelectedTag") && currentVal.getClass().toString().equals( "class java.lang.String")) { String tagString = (String) current.get(j); m_Editors[index].setAsText(tagString); currentVal = m_Editors[index].getValue(); } args[0] = currentVal; /* System.out.print("setterName: "+setter.getName()+ " editor class: "+m_Editors[index].getClass()+ " params: "); for (int k = 0; k < params.length; k++) System.out.print(params[k].toString()+" "); System.out.println(" value class: "+args[0].getClass().toString()); */ try { //we tell the setter for the current parameter to update the copy //with the current parameter value setter.invoke(copy, args); } catch (InvocationTargetException ex) { if (ex.getTargetException() instanceof PropertyVetoException) { String message = "WARNING: Vetoed; reason is: " + ex.getTargetException().getMessage(); System.err.println(message); Component jf; jf = m_ParentPanel.getRootPane(); JOptionPane.showMessageDialog(jf, message, "error", JOptionPane.WARNING_MESSAGE); if (jf instanceof JFrame) ((JFrame) jf).dispose(); } else { System.err.println(ex.getTargetException().getClass() .getName() + " while updating " + property.getName() + ": " + ex.getTargetException().getMessage()); Component jf; jf = m_ParentPanel.getRootPane(); JOptionPane.showMessageDialog(jf, ex .getTargetException().getClass().getName() + " while updating " + property.getName() + ":\n" + ex.getTargetException().getMessage(), "error", JOptionPane.WARNING_MESSAGE); if (jf instanceof JFrame) ((JFrame) jf).dispose(); } } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } //At this point we have set all the parameters for this GenericObject //with a single combination that was generated from the //m_WorkingSetCombinations Vector and can add it to the collection that //will be returned newGenericObjects.add(copy); } return newGenericObjects; } /** This method is responsible for returning all possible values through * a recursive loop. * * When the recursion terminates that means that there are no more parameter * sets to branch out through so all we have to do is save the current Vector * in the working set of combinations. Otherwise we iterate through all * possible values left in the next set of parameter values and recursively * call this function. * * @param previouslySelected stores the values chosen in this branch of the recursion * @param remainingValues the sets of values left */ public void combineAllValues(Vector previouslySelected, Vector remainingValues) { if (remainingValues.isEmpty()) { m_WorkingSetCombinations.add(previouslySelected); return; } Vector currentSet = new Vector((Vector) remainingValues.get(0)); Vector tmpRemaining = new Vector(remainingValues); tmpRemaining.removeElementAt(0); for (int i = 0; i < currentSet.size(); i++) { Vector tmpPreviouslySelected = new Vector(previouslySelected); tmpPreviouslySelected.add(currentSet.get(i)); combineAllValues(tmpPreviouslySelected, tmpRemaining); } } }