ERP 2.50:Developers Guide/Examples/Reference
Languages: |
Contents |
Introduction
This document aims to explain how to implement base references within modules. This is done using as example code snippets of an actual reference implementation.
Objective
The example used in this article is the UI Selector. It is a base reference that implements a new visualization for foreign key columns. Using SmartClient libraries, it displays a combo box that shows only the elements containing the the text that has been written in the input box, it is also possible to open a pop-up that displays a grid where is possible to filter by any of its columns. Further information about this Selector can be found here.
This document mainly explains the aspects that need to be taken into account when developing base references.
Concepts
The concept of reference is explained in the Data Model article. This article also gives an overview of all the base reference provided by core.
Creating additional base references within modules makes it possible to:
- define data types for columns
- develop custom visualization widgets for fields in generated windows
Once a base reference is created it is possible to define subreferences of the basereference. The purpose of this is to be able to have a generic implementation for the base reference with specific behavior implemented in the subreference using data which is defined on subreference level. The base reference in way defines the type of a reference (is it a search, a selector etc.), the sub-reference then defines a specific version, for example a selector reference to a business partner for example.
Base Reference and Sub Reference Model
For example the 'UI Selector' defines a base reference which defines the implementation of the selector. The implementation class defines how the selector rendered in each of the different views (edit mode, grid mode, search popup...)
Using this base reference as parent, multiple subreferences can be defined, each of them represents a concrete instance of the parent one. When creating columns in the application dictionary, the base reference can be used as reference, then in the reference search key, it will be possible to select on of the subreferences.
The base reference is the UI Selector itself. Then for each table we want to refer to, a new subreference is created defining which is the referred-to table and which columns should be shown in the popup and suggestion box. As mentioned above, this article will mainly focus on the base reference definition.
Application Dictionary Definition
References are defined in the Application Dictionary || Reference window. To define a new base reference, create a new reference checking the Base Reference field. After this, fill all the 3 implementation fields. These implementations are java classes that implement the behavior the reference has in different situations (DAL, WAD and UI). The different types of implementations are explained in detail in the following sections.
All references set as a base reference can be selected to be parent for subreferences. In the Selector example we have a base reference for Selector and we would define subreferences for each table we want to refer to.
Implementation
As mentioned in the previous section, references have implementations, these implementation are Java classes that are associated to a reference in the Application Dictionary and have the logic used by the reference.
Implementations can be defined at base or subreference levels. In case the subreference has defined its own implementations, they will be used, if not the ones in its parent one. In any case all references should have implementations at at least one of these two levels.
The 3 next points explain which is the purpose of each implementator and shows through an example how it can be implemented.
Model Implementation
The model implementation java class defines how the reference is used in the in-memory model of the Data Access Layer. It defines, for example, the java type for the column using the reference and how it is mapped in the Hibernate java mapping. The java type is used in the generated classes which represent the tables (in the src-gen folders). The java type is also used in the hibernate mapping itself.
Domaintype Interface Definitions
The model implementation must implement one of the following three interfaces:
- org.openbravo.base.model.domaintype.ForeignKeyDomainType: if the reference represents a foreign key reference
- org.openbravo.base.model.domaintype.EnumerateDomainType: if the reference defines a pre-defined list of allowed values
- org.openbravo.base.model.domaintype.PrimitiveDomainType: if the reference corresponds to a primitive type like String, number, date etc.
Openbravo has implementations for these interfaces for all relevant primitive types and also different foreign key types. They are all located in the org.openbravo.base.model.domaintype package. Each of the concrete implementations extend a base implementation:
- org.openbravo.base.model.domaintype.BaseForeignKeyDomainType
- org.openbravo.base.model.domaintype.BaseEnumerateDomainType
- org.openbravo.base.model.domaintype.BasePrimitiveDomainType
When creating a model implementation class it is strongly advised to extend one of these base domain types.
Implementing your own domain type
To implement a custom model implementation, start by creating a new class in your module. The class should extend one of the current Base*DomainType classes in the org.openbravo.base.model.domaintype package (see the Base*DomainTypes in the previous section). When extending one of the base domain types then most of the time only one or two methods need to be implemented. The javadoc of the methods gives information on the purpose of the method and there are many example domain type implementations available.
For example to implement a new primitive type one should extend BasePrimitiveDomainType, then only one method needs to be implemented (defined in the PrimitiveDomainType interface):
/** * The primitive type class (for example java.lang.Long) if this is a primitive type. * * @return the class representing the primitive type */ Class<?> getPrimitiveType();
After you have created your own implementation class set it in the Model Implementation field of the reference. Then re-compile the system. Then start the application, from then the new implementation class is used.
Note that extending existing Openbravo domain types not only gives re-use of the logic implemented in the parent class, but it also shields implementations from API changes as API changes are (if possible) implemented in base classes.
Bootstrap: initializing a DomainType
A domain type class can be simple but it can also be a complex class which reads information from several other places (database, configuration files, etc.) to initialize itself. For the first case, it is enough to extend a Base*DomainType and implement the required methods. In the latter case it is important to understand how a domain type is used in Openbravo.
A DomainType is used in two situations:
- Bootstrap: when the in-memory model is being initialized to determine types of columns etc.
- Runtime: at runtime to validate the value of a property
At runtime the DomainType can make use of standard Openbravo Data Access Layer concepts. At bootstrap time this functionality is however not available as the in-memory model is not yet initialized. This section discusses the specifics related to the bootstrap phase.
The DAL needs to be able to trigger a DomainType to initialize itself. To accomplish this the API of the DomainType defines an initialize method:
/** * Method is empty in this class, subclasses should override and call super.initialize() (to allow * future additional initialization in this class). * * Note: any subclass should clean-up and close database connections or hibernate sessions. If * this is not done then the update.database task may hang when disabling foreign keys. * * @see org.openbravo.base.model.domaintype.DomainType#initialize(org.openbravo.base.model.ModelProvider) */ public void initialize();
This initialize method is called when the in-memory model is being initialized. More specifically: when reading references from the database but before the rest of the in-memory model has been initialized.
As the DomainType needs to provide information during the bootstrap process it needs to be able to initialize itself. Some important aspects to take into account when implementing the initialize method:
- The initialize method is called before the in-memory model has been initialized, this means that the DomainType initialize method can not use the ModelProvider class to access the in-memory model.
- The initialize method must clean up and close database connections and hibernate sessions and session factories in its method body. If this is not done it is possible that the update.database task hangs.
Bootstrap: reading from the database
As the initialize method can not use the DAL directly it has to arrange its own data access. This can fairly easily be done by re-using the bootstrap model and hibernate configuration which is used by the in-memory model itself. The bootstrap model consists of a set of pojo's (Table, Column, Reference, etc.) which represent the model. These classes and their hibernate mapping can be found in the org.openbravo.base.model package.
To illustrate, how a DomainType can read data from the database we will refer to the Selector domain type. For this example assume that for a custom DomainType it is needed to read from a new table from the database which also references the rest of the model (Table, Column). Then the following needs to be implemented:
- a class representing this extra table, so a pojo with some getters and setters
- a hibernate mapping file in the same package as the class and the same name but instead of the extension java, the extension hbm.xml
- a few lines of code in the initialize method of the DomainType
For the selector there is a SelectorDefinition table which needs to be read during the initialization. The java representation of this table looks something like this (code truncated):
public class SelectorDefinition extends ModelObject { private Table table; private Column column; private String referenceId; public Column getColumn() { return column; } // more getters and setters }
Note the Column and Table are the bootstrap model types present in the org.openbravo.base.model package.
The corresponding hibernate mapping is stored in the SelectorDefinition.hbm.xml file in the same package, it looks like this:
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="org.openbravo.userinterface.selector.model.domaintype.SelectorDefinition" lazy="false" table="obuisel_selector"> <cache usage="read-write"/> <id name="id" type="string" column="obuisel_selector_id"> <generator class="uuid" /> </id> <many-to-one name="table" not-null="false" class="org.openbravo.base.model.Table" column="ad_table_id"/> <many-to-one name="column" not-null="false" class="org.openbravo.base.model.Column" column="ad_column_id"/> <property name="referenceId" not-null="true" column="ad_reference_id"/> </class> </hibernate-mapping>
The following code shows how to initialize and use hibernate. First a SessionFactoryController (a DAL class) is created, then the additonal class (reflecting the extra table) is added so that it can be mapped. Then the session is created.
final ModelSessionFactoryController sessionFactoryController = new ModelSessionFactoryController(); sessionFactoryController.addAdditionalClasses(SelectorDefinition.class); try { final Session session = sessionFactoryController.getSessionFactory().openSession(); session.beginTransaction(); // here read from the database using standard hibernate session.getTransaction().commit(); session.close(); } finally { // always force a close of the session factory sessionFactoryController.getSessionFactory().close(); }
Within the try/finally block the read actions can be done. The finally block releases all connections.
As a last note: a domain type is instantiated once per Reference. So if a domain type class is re-used in different references then it is instantiated multiple times.
Advanced: using custom hibernate types
The Openbravo reference functionality makes it possible to make use of the Hibernate custom user type, i.e. to fully customize how primitive values are stored in the database. The PrimitiveDomainType interface defines an additional method:
/** * The type used in the hibernate mapping. Most of the time is the same as the * {@link #getPrimitiveType()}. Can be used to set a hibnernate user type class. See the hibernate * documentation for more information on this. * * @return the class representing the hibernate type */ Class<?> getHibernateType();
Most of the time this method can just forward the call to the getPrimitiveType method (that's what is done in the BasePrimitiveDomainType class). However, for custom primitive domain types it is quite possible to return a Hibernate custom user type class. This makes it possible to customize the way Hibernate persists values to the database (using multiple columns for example). The current custom hibernate type has as main limitation that is not directly possible to define column or parameter tags inside the hibernate property mapping. This will be solved/extended in the future on the basis of user requirements.
WAD Implementation
This is the class in charge of creating the code for the generated windows with columns that use this reference. It extends org.openbravo.wad.controls.WADControl class. This class must be located in the module's src-wad directory. When WAD is compiled (ant wad) this class will be part of the openbravo-wad.jar library and will be used to create the code for generated windows. As WAD does not depend on DAL, it is not possible to use DAL to access to database in this class, the recommended way of doing so is through sqlc.
WADControl class implements a String reference which displays a simple input in the UI showing directly the column contents. Therefore subclasses should override all the methods which logic does not fit with this implementation.
UI Selector WAD Implementation
Bellow it is explained how is the implementation for the UI Selector example.
package org.openbravo.userinterface.selector.wad; ... public class WADSelector extends WADControl { public WADSelector() { } public WADSelector(Properties prop) { setInfo(prop); initialize(); }
This is the class that implements the WAD part for the UI Selector base reference. So in the application dictionary Reference window, we should set org.openbravo.userinterface.selector.wad.WADSelector in the WAD Implementation field.
Note that this class must extend WADControl. There is a constructor that receives a Properties object. In this properties there's information about the concrete column and field that uses the reference which can be used in other methods using the getData(propertyName) method implemented in WADControl, some of the most used properties are:
- ColumnName. Contains the name of the column that uses the reference.
- IsMandatory. Indicates whether the field is defined as mandatory in AD.
- IsReadOnlyTab. The whole tab is set as read only.
- IsUpdateable. The field can be updated.
- AdColumnId. ID of the column using the reference.
- ...
public void initialize() { addImport("StaticResources", "../../../../../web/../org.openbravo.client.kernel/OBCLKER_Kernel/StaticResources"); StringBuffer validation = new StringBuffer(); if (getData("IsMandatory").equals("Y")) { final String inpName = "inp" + getData("ColumnNameInp"); validation.append(" if (inputValue(frm." + inpName + ") === null || inputValue(frm." + inpName + ")===\"\") {\n"); if (getData("IsDisplayed").equals("Y")) validation.append(" setWindowElementFocus(sc_").append(getData("ColumnName")).append( ".selectorField").append(");\n"); validation.append(" showJSMessage(1);\n"); validation.append(" return false;\n"); validation.append(" }\n"); } setValidation(validation.toString()); setCalloutJS(); }
initialize method is called when the control is instantiated: once per field in the tab using the reference. It is used to add all the default JavaScript needed by the reference. In this case it adds the JavaScript imports (addImport), the JavaScript to validate the user has entered a value if the column is mandatory (setValidation) and the code to call a callout in case the column has one associated (setCallout). Note that these setters are implemented in the WADControl class and needn't be overridden in the subclass.
public String editMode() { final String[] discard = { "" }; if (!getData("IsMandatory").equals("Y")) { // if field is not mandatory, discard it discard[0] = "xxmissingSpan"; } final boolean isDisabled = (getData("IsReadOnly").equals("Y") || getData("IsReadOnlyTab").equals("Y") || getData("IsUpdateable").equals("N")); final XmlDocument xmlDocument = getReportEngine().readXmlTemplate( "org/openbravo/userinterface/selector/wad/WADSelector", discard).createXmlDocument(); xmlDocument.setParameter("disabled", (isDisabled ? "Y" : "N")); xmlDocument.setParameter("selectorLink", generateSelectorLink()); xmlDocument.setParameter("columnName", getData("ColumnName")); xmlDocument.setParameter("columnNameInp", getData("ColumnNameInp")); xmlDocument.setParameter("selectorVariable", "<script>var sc_" + getData("ColumnName") + " = null;</script>"); return replaceHTML(xmlDocument.print()); }
editMode() method returns a String with the HTML for the field in edition mode. In this example it uses a xmlEngine template where the dynamic parameters are set. Note a XmlEngine instance can be obtained using the getReportEngine() method implemented in WADControl class.
public String newMode() { return editMode(); }
newMode is similar to editMode, this one is used to get the HTML in edition mode for new records that have not already been saved. In our case as the HTML is exactly the same as the one in standard edit mode, editMode method is called.
public String toJava() { return ""; }
toJava method is used to get the Java code to be inserted in the generated Servlet to manage the reference. In this example there is no need of Java so it returns an empty String. If the HTML returned by editMode was a template requiring parameters to be set using xmlEngine, this toJava method should return the code to populate these parameters; in this case it would have been necessary to implement also the toXml() method, which generates the XML related to the reference with the parameters to be set in the HTML template.
public String getDisplayLogic(boolean display, boolean isreadonly) { StringBuffer displayLogic = new StringBuffer(); displayLogic.append(super.getDisplayLogic(display, isreadonly)); if (!getData("IsReadOnly").equals("Y") && !isreadonly) { displayLogic.append("displayLogicElement('"); displayLogic.append(getData("ColumnName")); displayLogic.append("_inp_td', ").append(display ? "true" : "false").append(");\n"); } return displayLogic.toString(); }
getDisplayLogic gets the JavaScript code called when the field using the reference has display logic associated. It receives 2 parameters display and isreadonly; the first one indicates whether the logic this method is returning is for displaying the field (true) or for hiding it (false), the second one is true if the field is set as readonly in dictionary. The JavaScript for display logic is usually a series of calls to displayLogicElement method passing as parameter the HTML objects that should be displayed or hidden.
public boolean isLink() { return true; }
isLink returns true in case the reference is a foreign key to any table. It is used to generate the field's label as a link to the referred tab. When it returns true, getLinkColumnId method should be implemented.
public String getLinkColumnId() { try { return WADSelectorData.getLinkedColumnId(getConnection(), subreference); } catch (Exception e) { return ""; } }
getLinkColumnId method complements to isLink one. If the reference is a foreign key this method must return the column ID the reference refers to. Depending on how it is defined in the reference, it can be a static value or it can be necessary to query to database to obtain it. In this example, it is in the subreferences were the information about the foreign key column is stored in; as the implementation is in the base reference, we query to database to retrieve which is the subreference's link. Note that it is using WADSelectorData class to perform the query, this is a sqlc class because as mentioned above at this point it is not possible to use DAL to access database.
Other important WADControl methods
WADControl base class has other methods that are not implemented in the Selector example because for this specific case they are not needed. However, they should be taken into account for other implementations.
The code snippets below show the most important ones, each followed by a description of their purpose.
public String columnIdentifier(String tableName, FieldsData field, Vector<Object> vecCounters, Vector<Object> vecFields, Vector<Object> vecTable, Vector<Object> vecWhere, Vector<Object> vecParameters, Vector<Object> vecTableParameters)
columnIdentifier method generates the SQL needed to display the column contents in the UI when it is part of the table identifier. This is done by adding Strings to the different Vectors passed as parameter. It should be implemented in case the actual value saved in database in the column with the reference is not the one that is going to be displayed to the user; for example in case of foreign keys, where instead of displaying the saved foreign ID, it will display the foreign record identifier.
In the case of the Selector example this method is not implemented, because even though the displayed value in the UI is not the same as the actual one stored in the database, the UI presentation is obtained in execution time using smart client.
public void processTable(String strTab, Vector<Object> vecFields, Vector<Object> vecTables, Vector<Object> vecWhere, Vector<Object> vecOrder, Vector<Object> vecParameters, String tableName, Vector<Object> vecTableParameters, FieldsData field, Vector<String> vecFieldParameters, Vector<Object> vecCounters)
processTable method is used to generate the SQL needed for the reference. It works in a similar way than columnIdentifier method.
Runtime UI Implementation
The runtime UI implementation is used to render the reference UI at runtime in the grid view and in the filter popup.
The class implementing this must extend org.openbravo.reference.ui.UIReference class.
UI Selector Runtime UI Implementation
The code snippets below show the methods implemented in the Selector example.
public class SelectorUIReference extends UIReference { public SelectorUIReference(String reference, String subreference) { super(reference, subreference); }
As mentioned before, the class that implements the Runtime UI must extend UIReference. It also needs to have a constructor with 2 Strings as parameters to set the reference and subreference IDs.
public void generateSQL(TableSQLData tableSql, Properties prop) throws Exception { boolean adminMode = OBContext.getOBContext().isInAdministratorMode(); OBContext.getOBContext().setInAdministratorMode(true); try { Reference ref = OBDal.getInstance().get(Reference.class, subReference); if (ref.getOBUISELSelectorList() != null && ref.getOBUISELSelectorList().get(0).getTable() != null) { UITableDir tableDir = new UITableDir("19", null); prop.setProperty("ColumnNameSearch", ref.getOBUISELSelectorList().get(0).getTable() .getDBTableName() + "_ID"); tableDir.identifier(tableSql, tableSql.getTableName(), prop, prop.getProperty("ColumnName"), tableSql.getTableName() + "." + prop.getProperty("ColumnName"), false); } else { super.generateSQL(tableSql, prop); } } finally { OBContext.getOBContext().setInAdministratorMode(adminMode); } }
This method is called to create the SQL query needed by TableSQLData class, which is used by generated windows to query to create the grid view as well as to navigate between records in the edit view. This method receives a TableSQLData object and a Properties one. The TableSQLData object should be modified by this method to include the required fields in the query. The Properties contains information about the current column with this reference; for example the column name can be obtained as prop.getProperty("ColumnName").
In this example, as Selector UI reference is a foreign key, we want to generate the query that displays not the actual value in database but the identifier for the referred element. This is done by getting from the Selector definition which is the table the selector references to, and using the generateSQL method in the tableDir for the given table.
public void generateFilterHtml(StringBuffer strHtml, VariablesSecureApp vars, BuscadorData field, String strTab, String strWindow, ArrayList<String> vecScript, Vector<Object> vecKeys) throws IOException, ServletException { UIReferenceUtility.addUniqueElement(vecScript, strReplaceWith + "/../org.openbravo.client.kernel/OBCLKER_Kernel/StaticResources"); strHtml.append("<td class=\"TextBox_ContentCell\">"); final String inputName = FormatUtilities.replace(field.columnname); strHtml.append("<script>var sc_" + inputName + " = null;</script>"); strHtml.append("<input type='hidden' name='inpParam" + inputName + "' id='" + inputName + "' value='" + field.value + "'"); strHtml .append(" onreset='sc_" + inputName + ".resetSelector();' onchange='openbravo.Utilities.updateSmartClientComponentValue(this, sc_" + inputName + ".selectorField);' "); strHtml.append("></input>"); strHtml.append("<script src='" + generateSelectorLink(field) + "'></script>"); strHtml.append("</td>"); }
generateFilterHtml method generates the HTML for the filter popup. This HTML code is appended in the StringButffer strHtml parameter. The information about the field that is part of the filter is passed in the BuscadorData field parameter. It is possible to add the required JavaScript imports by adding the path to the file in the ArrayList<String> vecScript parameter, in this example this is done using the UIReferenceUtility.addUniqueElement utility method, which inserts the String in the list only in case it has not been already inserted.
Other important UIReference methods
These methods are not implemented in the example but might be important for other references.
public String getGridType()
Is used to determine which is the format the reference must be rendered in grid view. Currently, the supported values are "string" (which is the default one), "img", "url", "float" and "dynamicEnum".
public String formatGridValue(VariablesSecureApp vars, String value)
This method is used to format the value to be shown in grid mode. By default it returns the value as it comes, without any additional formatting. It can be used in case the formatting depends on the session environment, rather than in the SQL query; for example to format numeric values.
Languages: |
ERP 2.50:Developers Guide/Examples/Data Access Layer and REST | ERP 2.50:Developers Guide/Common Issues, Tips and Tricks