The Source for Java Technology Collaboration
User: Password:



Rémi Forax's Blog

Community: JavaDesktop Archives


Mixing property language support and bean bindings

Posted by forax on September 24, 2007 at 03:14 PM | Permalink | Comments (21)

Recently, Shannon posts version 1.0 of beanbindings, even if this version is not ready for production use, it is stable enough to create a small demo mixing property language support and beanbindings.

The idea is to create a bean (here MyBean) with a bound property (label) and to bind this property to two different textfields.

properties-meet-bindings.png

Defining a bean

Because i use the property support directly integrated in the language, defining such bean is easy. AbstractBean provides the basic support for managing property change listeners.

 public static class MyBean extends AbstractBean {
    property String label bound;
    
    private <B,T> void propertyChanged(java.lang.Property<B,T> property,Object oldValue,Object newValue) {
      firePropertyChange(property,oldValue,newValue);
    }
    
    @Override
    public String toString() {
      return "label "+label;
    }
  }

Wrap the property to someting understandable

By default bean bindings only supports two kinds of property bean property that uses bean introspector and EL property that uses EL script but the framework is easily extensible and let you create your own property implementation.

So i've created a new kind of property (here LangProperty) that exposes a java.lang.Property as a property understandable by the bindings framework.

And mix

And now mixing the language support and the API together is as simple as that:

    LangProperty<MyBean,String> labelProperty =
        LangProperty.create(MyBean#label);
    
    JLabel label = new JLabel("Label:");
    JTextField field = new JTextField(20);
    Bindings.createAutoBinding(READ_WRITE, 
        bean, labelProperty,
        field, BeanProperty.create("text")).bind();
    
    JLabel anotherLabel = new JLabel("Another label:");
    JTextField anotherField = new JTextField(20);
    Bindings.createAutoBinding(READ_WRITE, 
        bean, labelProperty,
        anotherField, BeanProperty.create("text")).bind();

The notation sharp ('#') is used to get an objet typed java.lang.Property that allow to get and set the value of the property.

Want to test it

You want to test it by yourself, ok, here a zip containing the code, the beanbindings jar and the javac.jar patched to understand properties.
Download properties-meets-bindings.zip

Use this line to compile

 java -Xbootclasspath/p:javac.jar com.sun.tools.javac.Main -XDallowProperty -cp beansbinding-1.0.jar *.java

And this one to execute

 java -Xbootclasspath/p:javac.jar -cp .:beansbinding-1.0.jar Sample

You must include javac.jar at runtime only because it contains the class java.lang.Property and i am too lazy to create two jars.

WARNING, i have re-written the bound property support in the patched compiler last night so i think it will not work on another example :)

Cheers,
Rémi



Is closure swing ?

Posted by forax on August 29, 2006 at 04:08 PM | Permalink | Comments (9)

Yes, this is another entry about closure :)

In the closure proposal, the section "Closure conversion" describes how to use a closure instead of an anonymous class.
The rules are :

  • the implemented interface must have one method
  • the closure signature must be a subtype of the method signature

So let see if these rules are sufficient to use closures in order to implement AWT/Swing callbacks.

 

Implementing an ActionListener

ActionListener corresponds to a component default action, it's a one method interface, the method actionPerformed takes an ActionEvent that contains among other things the component that received the event.

  public interface ActionListener {
    void actionPerformed(ActionEvent e);
  }

So the client code can be this one :

 JButton button=new JButton("Ok");
 button.addActionListener(() {
   button.setText("Ko");
 });

Oh no !!!
This code is too optimistic, it assumes that because the event is not used, the parameter can be omit. But there is no such rule in the proposal. The correct code is the following :

 JButton button=new JButton("Ok");
 button.addActionListener((ActionEvent notUsed) {
   button.setText("Ko");
 });

The moral

It could be a good idea to add a closure conversion rule that permits parameters omission. Perhaps this rule can be extended to allow the omission of the last parameters from the right.

What do you think about that ?



Embed Scripts in a desktop application

Posted by forax on July 31, 2006 at 01:22 AM | Permalink | Comments (0)

After my previous post about Scripting for end users, i've made the promise to explain how to create a script aware desktop application. I think it's a good counterpart of the basics described in Scripting for the Java Platform.

So let me introduce a little OpenOffice Calc clone.

The spreadsheet is composed of cells, each cell contains two objects :

  • the source of the script and
  • a compiled version of the script.
The two objects are kept in memory in order to improve speed by using the source version during edition and the compiled version during evaluation.

private class Cell {
  final String text;
  final CompiledScript script;  
  public Cell(String text,CompiledScript script) {
    this.text=text;
    this.script=script;
  }
  @Override
  public String toString() {
    return text;
  }
}
In Java, it's fairly easy to create a the spread sheet because javax.swing.JTable is already a spreadsheet, you just have to hand code a TableModel. The following table model has one thousand rows and one hundred columns, all are editable and it delegates how to store a cell to method setCell() and how to retreive a cell to getCell().
private class ExCellTableModel extends AbstractTableModel {
  public int getRowCount() {
    return 1000;
  }
  public int getColumnCount() {
    return 100;
  }
  public Object getValueAt(int row, int column) {
    return getCell(row,column);
  }
  @Override
  public boolean isCellEditable(int row, int column) {
    return true;
  }
  @Override
  public void setValueAt(Object aValue, int row, int column) {
    String text=aValue.toString();
    CompiledScript script=null;
    try {
      script = compilable.compile(text);
    } catch (ScriptException e) {
      e.printStackTrace();
      return;
    }
    setCell(row,column,text,script);
    
    fireTableDataChanged();
  }
}
The method setValueAt is interresting, it is called when the user edit a cell. This method try to compile the script using a compilable engine (a script engine that can compile a script not just interpret it). After compiling the script, the method creates a new cell and informs the table component (JTable) to redraw the whole table using fireTableDataChanged(). We can't just redraw the edited cell because perhaps some other cells have scripts bound to the current cell.
Since we can have a lot of cells (1000*100), and most of the cells will be empty, i use a hashmap associating a cell to its position in order to not store empty cells.
private final HashMap<Long,Cell> cellMap=new HashMap<Long,Cell>();

Cell getCell(int row,int column) {
  return cellMap.get(mangle(row,column));
}
  
void setCell(int row,int column,String text,CompiledScript script) {
  cellMap.put(mangle(row,column),new Cell(text,script));
}
  
private static long mangle(int row,int column) {
  return (((long)row)<<32)+column;
}
The method main creates a script engine (here a javascript engine) using javax.script, the application and a JTable linked to the table model with a specific renderer. The rendrer evaluates (using eval) the cell's script each time a cell is rendered. This is not efficient but because the program doesn't maintain dependency between cells there is no way to do better. We need to re-evaluate all cells when one cell changed.
public static void main(String[] args) {
  ScriptEngineManager manager=new ScriptEngineManager();
  ScriptEngine engine=manager.getEngineByExtension("js");
    
  final SpreadTheWorld application=new SpreadTheWorld(engine);
  JTable table=new JTable(exCell.new ExCellTableModel());
  table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
    table.setDefaultRenderer(Object.class,new DefaultTableCellRenderer() {
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
      boolean isSelected, boolean hasFocus, int row, int column) {

      Cell cell=application.getCell(row,column);
      return super.getTableCellRendererComponent(table,
          (cell==null)?null:application.eval(cell),
          isSelected, hasFocus, row, column);
    }
  });
    
  JFrame frame=new JFrame("Spread the World");
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  frame.setContentPane(new JScrollPane(table));
  frame.pack();
  frame.setVisible(true);
}
The method eval() evaluates a cell and detects circularity using a set of visited cells.
private transient HashSet<Cell> circularity=
  new HashSet<Cell>();
  
private final Bindings bindings=new CellBindings();
final Compilable compilable;
  
public SpreadTheWorld(ScriptEngine engine) {
  this.compilable=(Compilable)engine;
}
  
Object eval(Cell cell) {
  if (cell.script==null)
    return cell.text;
    
  if (!circularity.add(cell)) {
   throw new IllegalStateException("circular refs");
  }
  try {
    return cell.script.eval(bindings);
  } catch (ScriptException e) {
    return e;
  } finally {
    circularity.remove(cell);
  }
}
Because cell script can refer to other cells, script.eval() take a hand coded binding as parameter.
A bindings (javax.script.Bindings) is a symbol table of all objects that are accessible from a script. Here, the bindings understands variables that match the regex "letters followed by digits" and returns an evaluation of the corresponding cell (the top left cell is referenced by a0).
private class CellBindings extends SimpleBindings {
  @Override
  public boolean containsKey(Object key) {
    return CELL_REF_PATTERN.matcher(key.toString()).matches() ||
      super.containsKey(key);
  }
    
  @Override
  public Object get(Object key) {
    Matcher matcher=CELL_REF_PATTERN.matcher(key.toString());
    if (!matcher.matches())
      return super.get(key);
    Cell cell=getCell(Integer.parseInt(matcher.group(2)),
      getColumnIndexFromName(matcher.group(1)));
    if (cell==null)
      return 0;
    return eval(cell);
  }
}
  
static final Pattern CELL_REF_PATTERN=Pattern.compile("(\\p{Alpha}+)(\\p{Digit}+)");

static int getColumnIndexFromName(String columnName) {
  int value=0;
  for(int i=0;i<columnName.length();i++) {
    value=value*26+Character.toLowerCase(columnName.charAt(i))-'a';
  }
  return value;
}

The above code of the bindings is not legal because a bindings inherits from java.util.Map so it must provide a way to iterate over all variables contained in the bindings. But it seems to work with the javascript engine but perhaps it will not work with another engine or with a newer version of Rhino.
I'm too lazy to update that code now but perhaps i will do it in a future blog entry.

The full code is available here !





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds