3D PLM Enterprise Architecture |
User Interface - CATJDialog |
Writing Stateless ControllersBest practices to design stateless controllers |
Technical Article |
AbstractThis article gives you several rules and best practices to help you designing stateless JDialog controllers. This is a key topic if you want your application to scale. |
In an application server environment, there is no choice between memory or
CPU consumption. The critical resource in such an environment is definitely
memory. Only applications with low memory usage (per user session) will scale.
Obviously CPU consumption is the price to pay for a low memory usage.
The application has then to try to be "as stateless as possible".
For example, an application that displays a query result in a table should
not hold the result data between two client requests. Indeed, what would happen
if the user never connects again after the first request? The server would have
to wait until the session timeout to release the result data (that may be
huge!).
Therefore - even if it's CPU consuming - the application has to (re) make the
query at each client request, and drop the result at the end of the request.
Some definitions:
[Top]
JDialog provides the application with a collection of graphical components
(widgets).
Those widgets are aimed at showing application data to the user.
From the widget point of view, we call it presentation data.
For example, for a TextField, the presentation data is the displayed text. For a DateEditor, the presentation data is the displayed date. For a Tree, the presentation data are the tree nodes...
Most of JDialog widgets hold the presentation data:
So - as a matter of fact - JDialog is not stateless (because it holds
data throughout the session).
Anyway, holding those data is not a major concern as their size is
"reasonable" and well controlled.
But there are some widgets that display an uncontrolled amount of data (that may be huge):
Those widgets have a specific design not to hold the presentation data:
[Top]
When using a tree or a table widget, the application has to provide the
widget with a data model object, that is in charge of feeding the widget
with presentation data.
Those models MUST be stateless (otherwise it may hold a huge amount of data).
This design allows JDialog to request presentation data only when rendering the tree or table widget to the Graphical User Interface (GUI), and to forget it right after that.
For historical reasons and for addressing different needs, the tree and table widgets allow several types of models:
Here are allowed model types for the Table widget:
Allowed models for Table | Stateless | Reserved methods on CATTable |
CATTableModel | NO |
|
CATKeyTableModel | YES |
|
Here are allowed model types for the Tree widget:
Allowed models for Tree | Stateless | Reserved methods on CATTree |
CATTreeModel | NO |
|
CATKeyTreeModel | YES |
|
CATKeyPathTreeModel | YES |
|
As you see in the previous charts, CATTreeModel and CATTableModel are not stateless compliant. Therefore you should not use them if you wish to write a stateless application. In the next chapter, we'll have a deep sight into CATKeyTableModel, CATKeyTreeModel and CATKeyPathTreeModel usage.
[Top]
The allowed stateless models are all key models: they identify widget items with a key (that is a string identifier).
Note: the tree 'item' is a node, the table 'item' is a row.
[Top]
at render time, the model is asked for presentation data + keys
when an event occurs on the GUI, the source item (s) is (are) identified with its (their) key (s)
Ex: a Tree with a CATKeyTreeModel with a controller listening to selection notifications: At render time, the model is asked for each visible tree node:
Later on, the user selects a node:
|
[Top]
Basically, the key should contain enough data for the application to identify clearly the related data.
Ex: a Table that shows a database query result If a row presented in the table corresponds to a database n-uplet, then the key should obviously be the database table key. If a table row corresponds to a combination of database n-uplets, then the key should be a combination (concatenation) of the required database tables keys. |
[Top]
When using a CATKeyPathTreeModel, tree nodes are no longer identified by the node key, but by the keys path from the root to the concerned node.
A key path is of type String[] and has the following format:
{<root node key>, <first children key>, [...], <the concerned node key>}
There are two cases where the application should use a CATKeyPathTreeModel instead of CATKeyTreeModel:
when 2 or more tree nodes may represent the same application data
when an event on a tree node has an impact on its parent nodes, and the application has no way of retrieving the parents keys programmatically (bad database design?)
Notice that identifying nodes with their keys path is heavier for the client and JDialog renderer, so applications should really be careful of using a CATKeyPathTreeModel only when needed.
[Top]
A stateless model should retrieve its application data only once per client request, and should not keep it beyond the request lifetime. There are two cases:
If an information is requested only once per client request and through a single method, then the model should simply retrieve application data in the method, and forget it right away,
If an information may be requested several times in the client request lifetime, or if several application data from the same source are requested through different methods, then the model should use request caching (not to process several times the same database query during the client request).
Request caching is performed through the session volatile properties:
CATSession.getVolatileProperty(String iName);CATSession.setVolatileProperty(String iName, Object iData);
Volatile properties is a memory space related to the client request. It has two advantages on other ways of caching transient data:
it is automatically destroyed at the end of the client request (you shall not worry about dropping your application data),
it is thread-safe: if the same model object is called at the same time by 2 or more different client requests (threads), there is no risk that the end of a request destroys data that is still required in an other running request.
[Top]
JDialog requests 2 kinds of information when rendering a tree:
the root key,
node information (label, icon, children keys ...) for displayed nodes.
Notes:
the root key may be static (hardcoded value), or dynamic (depending on the user for example).
node information is only requested once per client request and through a single method (getNodeInfo()). Therefore, no caching is required.
Let's imagine we want to write a JDialog tree that presents data from a database:
the root key is dynamic and depends on the user,
node information is stored in the database (in a single n-uplet).
Here is an implementation of this use case:
package com.dassault_systemes.myapplication; import com.dassault_systemes.catjdialog.CATKeyTreeModel; import com.dassault_systemes.catjdialog.CATKeyTreeModelCtxMenuEx; import com.dassault_systemes.catjdialog.CATMenuModel; import com.dassault_systemes.catjdialog.CATTreeNodeInfo; public class MyStatelessKeyTreeModel implements CATKeyTreeModel, CATKeyTreeModelCtxMenuEx { private String _rootKey; /** * MyStatelessKeyTreeModel constructor */ public MyStatelessKeyTreeModel(CATSession iSession) { // --- compute the root Key for the current user // [ TODO ] } /** * @see CATKeyTreeModelCtxMenuEx.getContextualMenu() */ public CATMenuModel getContextualMenu( String iKey ) { // --- retieve contextual menu model from the application data // --- this info is only requested once per client request, so caching is not necessary // [ TODO ] return null; } /** * @see CATKeyTreeModel.getRootKey() */ public String getRootKey() { // --- root key has already been computed return _rootKey; } /** * @see CATKeyTreeModel.getNodeInfo() */ public CATTreeNodeInfo getNodeInfo(String iKey, boolean iGetChildren) { // --- retieve node info from the application data // --- this info is only requested once per client request, so caching is not necessary // [ TODO ] return null; } } |
Notes:
Here, the root key is computed in the model constructor, and cached (as a class attribute) throughout the model lifetime. This is "acceptable" session caching (because the amount of cached data is known and controlled: 1 string value).
The model may require the user session, or even the tree object (for accessing message catalogs for example). Keeping the session (or widget) object as a class attribute in the model is allowed.
[Top]
JDialog requests 3 kinds of information when rendering a table:
column information (number, title, is sortable, ...),
total number of rows,
cell information (label, type, icon, ...) for displayed rows (from an offset to a number of displayed rows).
Notes:
Column information may be static (ex: we know in advance that the table shows "Name", "Value" and "Date"), or dynamic (defined in the database).
total number of rows and cell information are - in general - fully dynamic.
Unlike in the tree models, cell information are requested through several methods (getCell(), getType(), getIcon(), ...). In that case, application data should be queried once and cached in the session volatile properties.
Let's imagine we want to write a JDialog table that presents data from a database:
displayed columns are dynamic (stored in the database),
cell information (label, type, icon, ...) is stored in the database.
Here is an implementation of this use case:
package com.dassault_systemes.myapplication; import com.dassault_systemes.catjdialog.CATDialog; import com.dassault_systemes.catjdialog.CATKeyTableModel; /** * This table model represents a database table: * - displayed columns are customizable and set in the database * - cell information (label, type, icon, ...) is stored in the database */ public class MyStatelessKeyTableModel extends CATKeyTableModel { private CATDialog _dialog; /** * StatelessKeyTreeModel constructor * This model type needs the related dialog component (a tree) */ public MyStatelessKeyTableModel (CATDialog iDialog) { _dialog = iDialog; } // ===================================================================================== // === COLUMN INFORMATION // ===================================================================================== /** * This method returns column related data and manages caching through Session Volatile Properties */ private MyColumnsData getColumnsData(CATDialog iDialog) { // --- check whether the column data is cached or not MyColumnsData data = (MyColumnsData)iDialog.getSession().getVolatileProperty(iDialog.getPath()+"&Column"); if(data != null) return data; // --- retrieve columns data // [ TODO ] // --- cache the data for the request lifetime iDialog.getSession().setVolatileProperty(iDialog.getPath()+"&Column", data); return data; } /** * @see CATKeyTableModel.getColumnCount() */ public int getColumnCount() { MyColumnsData colData = getColumnsData(_dialog); // --- return column count from colData // [ TODO ] } /** * @see CATKeyTableModel.getColumnTitle() */ public String getColumnTitle(int iColumn) { MyColumnsData colData = getColumnsData(_dialog); // --- return column title from colData // [ TODO ] } /** * @see CATKeyTableModel.isColumnSortable() */ public boolean isColumnSortable(int iColumn) { MyColumnsData colData = getColumnsData(_dialog); // --- return whether the column is sortable or not from colData // [ TODO ] } // ===================================================================================== // === ROW COUNT INFORMATION // ===================================================================================== /** * @see CATKeyTableModel.getKeyCount() */ public int getKeyCount() { /* * Two choices: * ------------ * 1- make a complete query in database and only render the rows in the table display range * 2- make a first query for counting total number of results, * and then make another query (with results) when asked for * * The second choice is probably optimal. * Let's imagine we are in the second choice. */ // --- check whether the column data is cached or not Integer count = (Integer)_dialog.getSession().getVolatileProperty(_dialog.getPath()+"&Count"); if(count != null) return count.intValue(); // --- make the 'count' request // [ TODO ] // --- cache the data for the request lifetime _dialog.getSession().setVolatileProperty(_dialog.getPath()+"&Count", count); return count.intValue(); } // ===================================================================================== // === CELL INFORMATION // ===================================================================================== /** * This method returns rows related from Session Volatile Properties * (doesn't check whether is is valuated or not) */ private MyRowsData getRowsDataFromCache(CATDialog iDialog) { // --- check whether the column data is cached or not return (MyRowsData)iDialog.getSession().getVolatileProperty(iDialog.getPath()+"&Rows"); } /** * @see CATKeyTableModel.getKeys() */ public void getKeys( int iOffset, String [] oKeys ) { /* * This is the first called method before retrieving row informations * It manages the database query * It is only called once per client request */ // --- make the query, and keep it in a MyRowData object // [ TODO ] // --- cache the data for the request lifetime _dialog.getSession().setVolatileProperty(_dialog.getPath()+"&Rows", rowsData); // --- return keys from rowsData // [ TODO ] } /** * @see CATKeyTableModel.getType() */ public int getType(String iKey, int iColumn) { MyRowsData rowsData = getRowsDataFromCache(_dialog); // --- (rowsData is valuated as getKeys() was called before) // --- return cell type from rowsData // [ TODO ] } /** * @see CATKeyTableModel.getCell() */ public String getCell(String iKey, int iColumn) { MyRowsData rowsData = getRowsDataFromCache(_dialog); // --- (rowsData is valuated as getKeys() was called before) // --- return cell label from rowsData // [ TODO ] } /** * @see CATKeyTableModel.getState() */ public boolean getState(String iKey, int iColumn) { MyRowsData rowsData = getRowsDataFromCache(_dialog); // --- (rowsData is valuated as getKeys() was called before) // --- return cell state from rowsData // [ TODO ] } /** * @see CATKeyTableModel.getImage() */ public String getImage(String iKey, int iColumn) { MyRowsData rowsData = getRowsDataFromCache(_dialog); // --- (rowsData is valuated as getKeys() was called before) // --- return cell icon from rowsData // [ TODO ] } /** * @see CATKeyTableModel.getLink() */ public String getLink(String iKey, int iColumn) { MyRowsData rowsData = getRowsDataFromCache(_dialog); // --- (rowsData is valuated as getKeys() was called before) // --- return cell link from rowsData // [ TODO ] } } |
Notes:
MyColumnsData may be any class that holds columns data (it may be a simple Hashtable or Vector for example)
MyRowsData may be any class that holds rows data (it may be a SQL result set for example)
start() and stop() methods (inherited from CATRequestListener) don't need to be overloaded as the application data cleanup is automatically done by the volatile properties
Here, we know that column information may not change throughout the session. We may have retrieved column information in the model constructor and kept it as a class attribute (not to retrieve it at each new client request). It would be "acceptable" session caching as we may assume this information always has a reasonable size.
Session caching row count information is a bit touchier. This data has definitely a reasonable size, but the problem here is more a functional matter: what if the real row count changes during the user session (for example a row is removed by another user)? This may introduce bugs. If row count can't change at runtime, then it is also "acceptable" session caching (*see warning 1 below*).
Obviously cell information caching is not acceptable as this represents uncontrolled amount of data (notwithstanding it may probably change during the session).
In order to store cached data in a safe place in the volatile properties, the property name is prefixed with the related widget's path (supposed to be unique in the session). This avoids collision with other models (and even the same model instantiated for another widget in the same page!). (*see warning 2 below*)
Warning 1: Even request caching the row count may be unsafe as the count request and the cells information request are probably performed with 2 distinct requests and that data may have changed in the meantime. So be aware that row count may have changed when you request cells information.
Warning 2: Prefixing volatile properties name with the widget's path forces the model to hold a reference to the widget (given by the controller in the model constructor). That's a problem with CATKeyTableModel design: the widget object should be passed to the model in each method (actually this is done in the new CATTreeKeyPathModel).
[Top]
Version: 1 [Jul 2002] | Document created |
[Top] |
Copyright © 2000, Dassault Systèmes. All rights reserved.