The Source for Java Technology Collaboration
User: Password:



Rémi Forax's Blog

July 2006 Archives


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 !



Scripting for end users

Posted by forax on July 28, 2006 at 04:54 AM | Permalink | Comments (4)

After reading the John O'Conner Blog about scripting, i want to share some thoughts.
First, embedding script in a desktop application is not new, Mozilla Firefox, Open Office Writer/Calc (Microsoft Word/Excel) already provide a scripting environment to their end users since years.

While i'm not convinced by the fact that applications fully written with script are easy to maintain, i fully agree with javakiddy when he writes this comment about john's prose:
"Scripting is very useful in desktop applications, but generally it should be employed for the benefit of the application or the end user, not as a convenience to the programmer. ;)"

Furthermore providing a way to define scripts to end users can help you to understand what enhancement the customer want.
I love the mozilla strategy on that :

  1. let users create their own extension using script (javascript)
  2. if a script is popular integrate it in the plateform by frozen it in C++.

So perhaps it should not be be a bad idea to see a language that ease to define a program in a script manner and then enable to froze some part by typing them.
Eiffel already has this kind of concept, named melted ice i think, and i will love to see Java goes in that direction.

Ok, that's enought for today, in the next post, i will try to show a small desktop application demonstrating how to use javax.script.Bindings to let end users interact with Java object.



SwingWorker's process signature

Posted by forax on July 26, 2006 at 03:25 AM | Permalink | Comments (3)

Preamble : this is my first blog entry, so champagne!!

Ok, you have perhaps notice it, the signature of the SwingWorker's process method change in the last beta (b92), and you can blame someone for that ... me.

I'm proud to have reported this bug (6432565) even if for patch it, the interface of SwingWorker has been changed. After all the JDK is still in beta.

SwingWorker is used to publish data from an external thread (the worker thread) to the EDT (Event Dispatch Thread), because, in Swing, only the EDT can change GUI states.

The EDT is in charge of dispatch events, refresh the gui, etc. so the worker thread can call publish several times before the process method is called by the EDT, so the published data need to be gathered. Swingworker uses a list to store these data and then calls the process method. Before b92 a new array was created from that list and pass as parameter to the process method.

So where is the bug ? The bug comes when you try to allocate this array, it's a parameterized array (by T), and in Java at runtime you can't know the declared type used for T and THERE IS NO WAY TO DETERMINE IT EVEN USING REFLECTION.

So how to correct the bug, it's easy, use instead a list of T.

PS: I don't write this blog to blame the SUN ingeneer that wrote that code. For the record, two weeks after, on another bug i try to convince him that it was possible to guess the class of a type variable at runtime using a smart trick, of course, the proposed code was wicked.





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