|
|
||
Rémi Forax's BlogJuly 2006 ArchivesEmbed Scripts in a desktop applicationPosted 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 :
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.
The full code is available here ! Scripting for end usersPosted 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.
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:
Furthermore providing a way to define scripts to end users can help you to understand what enhancement the customer want.
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.
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 signaturePosted 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. | ||
|
|