BC4J/JClient Performance Study


Contents

Overview
Brief Overview of the 3-Tier BC4J Architecture
Why Swing and JClient?
General Comments About Three Tier Architectures
Using the Value Messenger Pattern to Minimize Round Trips
Measuring the Round Trips Your JClient/BC4J App Performs
The App We Received
The Application Functionality
The Issues We Found
Rebuilding the "Style Card" Application
The MC_TEST Sample Schema
The OptimizedRoundtrips.jws Workspace Structure
Building The StyleApp Application Module
Leveraging SQL to Improve the Client
Building The User Interface
Implementing Additional Network Optimizations
Network Performance of Rewritten Sample
Setting Up the Test Data Environment
Our Final Network Round Trip Measurements
Tips for Quicker Development
Other Issues
Summary

Overview

Oracle Consulting aske d the BC4J development team to review a sample JClient/BC4J application from one of their customers. We were specifically asked to study the number of thin-client-to-appserver roundtrips made by their sample application, and provide guidance on how to minimize these round-trips.

This paper documents the issues that we found in the example that influence round-trips and documents the rebuilt "best practices" sample application that we have produced to illustrate the theoretical minimum number of round-trips when the BC4J and JClient frameworks are used in the optimal way.

We illustrate that an equivalently-functional form can startup with 15-20 network roundtrips instead of the more than 100 round trips that the customer's analogous application was incurring.


NOTE:

The observations herein were made using the JDeveloper 9.0.3.2 maintenance release, as well as the upcoming 9.0.3.3 maintenance release.


Brief Overview of the 3-Tier BC4J Architecture

Why Swing and JClient?

Developers who demand an application with a cross-platform, rich user experience typically turn to Java and its built-in Java Foundation Classes. This library, also known as "Swing" from its early project code name, provides a powerful and portable set of UI building blocks so your application can run on any machine with a Java VM. The fact that the entire JDeveloper IDE is built using Swing is a solid testament to the richness of the user experience that a Swing application can offer end-users.

When a Swing application needs to present and interact with a corporate database -- a basic requirement of any kind of business application -- developers need to combine Swing with additional layers of code to handle interacting with the back-end business services that expose business-tier information, enforce business rules, and coordinate database access. While numerous books exist that explain the basic Swing libraries, virtually no books explain how to use Swing to build a rich-client business application on the J2EE platform.

This lack of reading material on the subject is not by accident. The simple explanation is that the code required to do a good job of coupling Swing with backend business services in a network-efficient way is simply too complex if you were to have to develop it yourself. While doing the job in a network-inefficient way is fairly straightforward, doing it properly requires clever coordination of a thin-client data cache and the middle-tier business services that provide data and allow that data to be modified.

J2EE developers choose the Oracle BC4J framework for simplifying the task since it includes all of the "plumbing" code to make building these kinds of rich-client business applications much simpler. Due to its unique design, your rich-client application can work either in a two-tier or three-tier deployment architecture, as well. The JClient data binding framework for Swing works together with BC4J and allows you to easily bind any Swing user interfaces to your back-end business services. It is architected to support anything that you can build using Swing, however custom, so for example you could use open source Swing-based libraries like JGoodies to build even nicer looking forms and panels with less page layout effort and without becoming masters of the powerful, yet complicated Swing layout mangers.

The three-tier deployment architecture is especially compelling due to the introduction of Java WebStart, which allows clients to download the latest version of your application client interface from a web server with the simplicity of a central point of update. However with this power, comes the responsibility to understand the nature of the three-tier application regarding network traffic. We'll see in this paper that by properly exploiting the features of the BC4J framework, your thin-client applications can work in a highly-optimized way over the network. However, as might be expected, not all of the optimizations can happen for you automatically...Read on to learn more.

General Comments About Three Tier Architectures

When the code that implements the user interface is physically separated from the application code that implements the business service, the two layers must communicate using some IPC mechanism. For thin-client Swing UI's working with remote EJB Session Beans, this is the RMI-over-IIOP protocol.

The frequency with which the thin client communicates with the remote business components is a determined by a combination of two primary factors:

  1. The number of server-side API calls performed by the client
  2. The amount of local data-caching and data-transfer optimizations that thin-client layer is able to perform

The BC4J framework contains a thin-client cache and a sophisticated mechanism to keep this thin-client cache in sync with middle-tier data that the user interface is displaying and interacting with. This design pattern, unique to BC4J, is our "Value Messenger" design pattern implementation. If developers exploit this feature correctly, they can keep network roundtrips to a minimum by pushing as much logic as possible into their middle-tier components.

BC4J framework components like Entity Objects, Associations, View Objects, View Links, and Application Modules already are designed to automate a large amount of common application infrastructure code for your middle-tier business components. By making maximal and correct use of these built-in framework features, you not only can achieve fewer network round trips, but also have much less hand-written "plumbing" code to write yourself.

Using the Value Messenger Pattern to Minimize Round Trips

The Simplifying J2EE and EJB Development with BC4J whitepaper on Oracle Technet describes the BC4J architecture and functionality in detail. In particular, it explains the BC4J implementation of the "Value Messenger" pattern in the section entitled "Understanding the BC4J Value Messenger Pattern Implementation". The Value Messenger design pattern allows the developer to place a maximal amount of application logic into the middle tier components (instead of writing it inside the thin client classes), since it automatically coordinates keeping the thin-client cache in sync with any changes that this middle-tier code causes to occur.

For example, if you are building a form that edits a "master" record and some of its "detail" information -- perhaps assisted by data-driven poplists as well -- you need to coordinate a lot of data sources before you can display the form to the user. You can achieve all of this data model "setup" on the server side using a custom application module method combined with view links between master and detail views. In a single remote method call from the thin client -- perhaps passing in a key ID number of the master "thing" you want to edit -- all of the data needed by the form is retrieved on the server side. Thanks to the value messenger implementation, when control returns to your thin client from the server-side custom method, all of the changes in the data being observed by your Form are brought back in the same round-trip response.

Using BC4J, you want to push as much application code as possible into the middle-tier business components, leaving only the bare minimum of client-side code in the thin client. This not only keeps the thin client thin, it also makes sure that the network round trips are the minimum necessary.

We'll see in this article that by correctly exploiting the features of the BC4J framework, we were able to achieve a dramatic reduction in the amount of client-side code and network round trips, when compared with the example application provided to us from the customer (via Oracle Consulting) at the outset.

Measuring the Round Trips Your JClient/BC4J App Performs

JDeveloper comes with three built-in performance profiling tools:

  • The Execution Profiler

    This lets you find "hot spots" in your Java code on the client and server side to know where to spend time optimizing.

  • The Memory Profiler

    This helps you find objects that you might have inadvertently kept references to such that the VM's garbage collector cannot free them as you expected.

  • The Event Profiler

    This helps get a higher-level picture of where time is being spent in your application doing things like executing queries, issuing remote method calls, loading metadata, or any number of developer-defined, application-specific events.

The BC4J framework is instrumented already with a number of diagnostic events that the JDeveloper profiler can show you. Figure 1 shows what project-level profiling filter options can be set for BC4J. In the screen shot shown here, we've configured the Event Profiler to only show JVM garbage collection events ("GC") as well as BC4J remote method calls. For this exercise, we were more interested in the number of remote method call round trips and no so interested in the timing information, so we've unchecked all of the detail information columns except for the "Comment" that will tell us which remote methods are getting invoked.

Project-Level Profiler Filters to See BC4J Remote Method Calls
Figure 1: Project-Level Profiler Filters to See BC4J Remote Method Calls

Given our event profiler settings above, the output of using the profiler on a JClient application running against a remotely deployed application module looks like what you see in Figure 2 .

Display of the JDeveloper Event Profiler
Figure 2: Display of the JDeveloper Event Profiler

The App We Received

Oracle Consulting supplied us with the TarExample application that has the user interface shown in Figure 3 . They said this application represented one of the forms that the customer has built.

Style Card Form from the Customer's TarExample Application
Figure 3: Style Card Form from the Customer's TarExample Application

The Application Functionality

As we understood it, the "Style Card" form is used to edit information about the styles related to an apparel collection for a given department. When the form is run, a particular "collection ID" is passed in, and the style information for that collection is edited. It appeared from the data model that a Collection is related to exactly one Department.

 

  • The main panel at the top shows Style Record information.
  • The "Accessories" area allows the user to see, create, and delete accessory detail information for the style.
  • The "Labels" area allows the user to see and modify the different kinds of labels that might be related to the style (barcode labels, main labels, size labels, and hangtag labels). It also allows the user to enter or remove miscellaneous other kinds of labels in the table below.
  • The "Group Info" area appeared to be for reference information only and displayed the set of StyleGroups related to the current department, as well as the related set of StyleSubGroups related to the currently selected StyleGroup.
  • The (Ok) button at the bottom saves any changes made to the style, while the (Cancel) button aborts any changes and exits the form.

 

The Issues We Found

Upon running the first versions of the TarExample application supplied by Oracle Consulting we used the JDeveloper event profiler to observe over 100 network roundtrips between the thin-client UI and the application module deployed as an EJB session bean. This was measured from the time the form was run, until it appeared with its data populated. This TarExample already contained some suggested modifications to the original customer application which the BC4J team had made to Oracle Consulting during previous, internal conference calls.

We highlighted a number of the factors of the application's design that contributed to this high number of network roundtrips:

  1. Application Module Did Not Use View Links

    Instead of using middle-tier, built-in functionality of the BC4J framework to coordinate master/detail view objects, the user interface classes contained client-side code that was manually performing the master/detail coordination. It carried out the task of retrieving the current primary key values from a master record and setting bind variables in multiple, related view objects to produce a coordinated set of detail records.

    By declaratively defining view links where master/detail relationships exist, all of this coordination is done on the server side with no round trips and much less code.

  2. Client-side Code to Do Query By Example

    Instead of passing the required collection id into a custom method on the application module that could encapsulate all of the server-side data-model setup, the application contained client-side code to manually construct and append a where clause and execute a query to find the appropriate style record.

    Also, the manual code written to build up WHERE clauses gave us the impression that the customer was not aware that BC4J provides a built-in ViewCriteria feature in all view objects that implements a sophisticated query by example for you with no hand-written code required to build up the WHERE clauses.

  3. Client-side Code to Coordinate JComboBoxes

    The data driving the LOV comboboxes were all detail queries requiring the context of the current department to execute. The application contained code that was manually coordinating the bind variable population and execution of the queries from the client instead of letting the server-side component handle all of this detail via view links.

  4. Client-side Calls to Clear View Object Caches

    It was not obvious to us why, but many places in the application appeared to be making client-side calls to clear the view object caches.

  5. Client-Side Code to Manage Poplist Selection for Style Label Details

    The details of the four preset kinds of style labels are stored in the STYLE_LABELS detail table along with the miscellaneous label information. Each style can have either zero or one of the preset label kinds, and in fact the user interface presents these detail rows as if they were a simple property of the master record. There was a lot of client-side code that was creating blank rows in the StyleLabels view object to fill out blank rows for the four different preset label kinds, as well as client-side code that was coordinating the label poplists with the data in this detail table.

Rebuilding the "Style Card" Application

We decided that the most educational feedback we could provide, beyond highlighting the above specific issues, was to rewrite the "Style Card" form using a number of "best practices" techniques to minimize network round-trips. In this section, we highlight the interesting details of the "OptimizedRoundtrips.jws" workspace containing this rewritten version of the application.

The MC_TEST Sample Schema

Figure 4 shows the Database diagram (produced with JDeveloper 10g Preview) of the tables we received in the MC_TEST schema.

Database Diagram of Customer's Sample Schema
Figure 4: Database Diagram of Customer's Sample Schema

The OptimizedRoundtrips.jws Workspace Structure

We created a workspace with two projects:

  • Model.jpr

    This contains the model layer components that implement the business service, as well as the EJB Session Bean which the BC4J design time automatically creates for us when we deploy the application module component as an EJB (Session Facade, Bean Managed Transaction). The components have been organized into packages with appropriate names in the customer.app.model.* package hierarchy.

  • View.jpr

    This contains the view layer components that implement the user interface of the application. The panels and form live in appropriately named packaged in the customer.app.view.* package hierarchy.

Building The StyleApp Application Module

We created BC4J entity objects and associations by reverse-engineering the tables in the MC_TEST schema using the BC4J wizard. We opted not to immediately create any view objects or application module since we wanted to create these in a different package from the entities.

Next, we created view objects patterned after the view objects from the TarExample application, except that we built them to have the appropriate view links with the driving, parent view object. This generally meant copying the query from the TarExample application (where Expert Mode was being used) and removing the hand-coded bind variables that would be automatically added by the BC4J view link coordination feature. Figure 5 illustrates the view-linked data model that we ended up with for the StylesApp application module.

Application Module "Data Model" for Style Card App
Figure 5: Application Module "Data Model" for Style Card App

Leveraging SQL to Improve the Client

Having noticed that some of the client-side calls were related to the management of the Style Label information residing in a detail table, I decided to modify the TarExample's SQL query for the StyleRecords view object to add four new SQL-derived attributes:

  • BarcodeLabelId
  • MainLabelId
  • SizeLabelId
  • HangLabelId

I accomplished this by enhancing the "Expert Mode" query for StyleRecords to add the parts below that are flagged as /* ADDED */, in order that in each row of this rowset reflect its current values from the detail STYLE_LABELS table for the four built-in style label types. The way I've written the query with outer-joins, the lack of a detail row in STYLE_LABELS for any of the style label types (1,2,3 or 4), will return a NULL value for the current label id. It will also work as expected if a STYLE_LABELS row exists, but has a NULL value for LABELS_ID.

SELECT Styles.STYLES_ID, 
       Styles.STYLE_NAME, 
       Styles.SUPPLEMENT, 
       Styles.BLOCKED, 
       Styles.STYLE_NUMBER, 
       Styles.EAN_NO, 
       Styles.BOX, 
       Styles.STYLE_SUBGROUPS_ID, 
       Styles.CREATED_BY, 
       Styles.CREATED_DATE, 
       Styles.CHANGED_BY, 
       Styles.CHANGED_DATE, 
       Styles.DELETED,
       Styles.STYLE_PICTURES_ID,
       Styles.STYLE_DESCRIPTION, 
       Styles.QUALITY_DESCRIPTION,
       Styles.USERS_ID,
       Styles.BRANDS_ID,
       StyleColls.STYLE_COLLS_ID,
       StyleColls.STYLES_ID AS STYLES_ID1,
       StyleColls.COLLECTIONS_ID,
       StyleColls.DEPARTMENTS_ID,        
       StyleSubgroups.STYLE_SUBGROUP_NAME, 
       StyleSubgroups.STYLE_SUBGROUPS_ID AS STYLE_SUBGROUPS_ID1, 
       StyleGroups.STYLE_GROUP_NAME, 
       StyleGroups.STYLE_GROUPS_ID, 
       Collections.DEPARTMENTS_ID AS DEPARTMENTS_ID1,
       Collections.COLLECTION_TERM,
       Collections.COLLECTION_NAME,      
       Collections.COLLECTIONS_ID AS COLLECTIONS_ID1,
       Departments.DEPARTMENTS_ID_PRIOR, 
       Departments.DEPARTMENTS_ID AS DEPARTMENTS_ID2,
       Users.NETWORK_ID,
       barcodeLabel.LABELS_ID AS BARCODE_LABEL_ID, /* ADDED */
       mainLabel.LABELS_ID AS MAIN_LABEL_ID,       /* ADDED */
       sizeLabel.LABELS_ID AS SIZE_LABEL_ID,       /* ADDED */
       hangLabel.LABELS_ID AS HANG_LABEL_ID        /* ADDED */
FROM MC_TEST.STYLES          Styles,
     MC_TEST.STYLE_SUBGROUPS StyleSubgroups, 
     MC_TEST.STYLE_GROUPS StyleGroups, 
     MC_TEST.STYLE_COLLS StyleColls,
     MC_TEST.COLLECTIONS Collections,
     MC_TEST.DEPARTMENTS Departments,
     MC_TEST.USERS Users,
     MC_TEST.STYLE_LABELS barcodeLabel,    /* ADDED */
     MC_TEST.STYLE_LABELS mainLabel,       /* ADDED */
     MC_TEST.STYLE_LABELS hangLabel,       /* ADDED */
     MC_TEST.STYLE_LABELS sizeLabel        /* ADDED */
WHERE Styles.DELETED                  = 'N' 
  AND Styles.STYLE_SUBGROUPS_ID       = StyleSubgroups.STYLE_SUBGROUPS_ID
  AND Styles.USERS_ID                 = Users.USERS_ID(+) 
  AND StyleSubgroups.STYLE_GROUPS_ID  = StyleGroups.STYLE_GROUPS_ID
  AND Styles.STYLES_ID                = StyleColls.STYLES_ID
  AND StyleColls.COLLECTIONS_ID       = Collections.COLLECTIONS_ID
  AND Collections.DEPARTMENTS_ID      = Departments.DEPARTMENTS_ID
  AND barcodeLabel.STYLES_ID      (+) = Styles.STYLES_ID /* ADDED */
  AND barcodeLabel.LABEL_TYPES_ID (+) = 1                /* ADDED */
  AND mainLabel.STYLES_ID         (+) = Styles.STYLES_ID /* ADDED */
  AND mainLabel.LABEL_TYPES_ID    (+) = 2                /* ADDED */
  AND sizeLabel.STYLES_ID         (+) = Styles.STYLES_ID /* ADDED */
  AND sizeLabel.LABEL_TYPES_ID    (+) = 3                /* ADDED */
  AND hangLabel.STYLES_ID         (+) = Styles.STYLES_ID /* ADDED */
  AND hangLabel.LABEL_TYPES_ID    (+) = 4                /* ADDED */

This query will make presenting and data binding for the four label poplists much simpler and make it possible to achieve the same behavior as the original application without code.

Also, instead of managing the rows in the detail StyleLabels view object from client-side code, I generated a custom row class for the StyleRecords view object (including accessors), and then overrode the way that the set attribute code was performed for these four new attributes. The correspondingly overridden setter methods in the StyleRecordsRowImpl.java class look like what you see below. Each one has a single additional line added to call a private helper method called modifyDetailStyleLabel.

  public void setBarcodeLabelId(Number value) {
    setAttributeInternal(BARCODELABELID, value);
    modifyDetailStyleLabel(BARCODELABELID,value); /* ADDED */
  }

  public void setMainLabelId(Number value) {
    setAttributeInternal(MAINLABELID, value);
    modifyDetailStyleLabel(MAINLABELID,value); /* ADDED */
  }

  public void setSizeLabelId(Number value) {
    setAttributeInternal(SIZELABELID, value);
    modifyDetailStyleLabel(SIZELABELID,value); /* ADDED */
  }
  public void setHangLabelId(Number value) {
    setAttributeInternal(HANGLABELID, value);
    modifyDetailStyleLabel(HANGLABELID,value); /* ADDED */
  }

The modifyDetailStyleLabel helper method does the work from the server side of coordinating the setting of the four preset label ids with their corresponding rows in the detail StyleLabels view object. The straightforward code for that routine looks like this, and it also lives in the StyleRecordsRowImpl.java class:

 /**
   * Modify the appropriate detail row in the view-linked StyleLabels
   * when the SQL-Derived attribute in the StyleRecords row is set by
   * the user. Since this is done in middle-tier code, it causes no
   * thin-client-to-appserver traffic.
   */
  private void modifyDetailStyleLabel(int ATTR, Number labelsId) {
    RowIterator styleLabels = getStyleLabels();
    boolean foundDetailLabelRow = false;
    Number typeToUpdate = styleLabelTypeCodeForAttr(ATTR);
    while (!foundDetailLabelRow && styleLabels.hasNext()) {
      StyleLabelsRow slr = (StyleLabelsRow)styleLabels.next();
      if (slr.getLabelTypesId().equals(typeToUpdate)) {
        slr.setLabelsId(labelsId);
        foundDetailLabelRow = true;
      }
    }
    if (!foundDetailLabelRow) {
      StyleLabelsRow newStyleRow = (StyleLabelsRow)styleLabels.createRow();
      newStyleRow.setLabelsId(labelsId);
      newStyleRow.setLabelTypesId(typeToUpdate);
      /* Note: StyleLabelId attribute is of type DBSequence and will be
       * ----  auto-assigned from the customer's existing DB sequence
       *       by the BEFORE INSERT FOR EACH ROW trigger created by the
       *       customer's script on the STYLE_LABELS table.
       */
    }
  }

To encapsulate the mapping of the numeric "type" code for each of the preset label kinds, I chose to implement the further helper method called styleLabelTypeCodeForAttr like this:

  private Number styleLabelTypeCodeForAttr(int ATTR) {
    switch (ATTR)  {
      case BARCODELABELID: return new Number(1);
      case    MAINLABELID: return new Number(2);
      case    SIZELABELID: return new Number(3);
      case    HANGLABELID: return new Number(4);
    }
    return null;
  }

The net result is that when any of the four new SQL-derived label id attributes is updated in the StyleRecords row, behind the scenes -- and of particular importance, in the middle-tier -- the corresponding rows of the StyleLabels view object are maintained. When the transaction is committed, any modified rows will be committed along with any other edits performed by the user.

Building The User Interface

To build the user interface, we used the default features of JDeveloper for building JClient-based Swing data panels and forms. We used the JDeveloper UI Editor to layout the forms, and where necessary, declaratively setup the data binding code.


NOTE:

We did not try to copy the user interface exactly, but tried instead to represent the same number of zones, tables, and poplists that the sample had. Showing additional fields from the StyleRecords view object on the StyleRecordsPanel, for example, would not affect the network round trips. Remember that we were working with a bare minimum amount of customer data, so some of the poplists in the TarExample were coming up blank for us when we ran it. If the actual customer application contains more poplists to populate, the tips in this paper should apply directly to optimize their population as well.


Building the PanelStyleRecords Panel

We started by using the "Panel" wizard in the New Gallery's "Swing/JClient for BC4J" category to create a default form panel for the StyleRecords view object. The wizard produced a databound panel that looked like Figure 6

PanelStyleRecords.java in the UI Editor
Figure 6: PanelStyleRecords.java in the UI Editor

Building the PanelStyleAccessoriesDescriptions Panel

Next we built the PanelStyleAccessoriesDescriptions panel by again using the "Panel" wizard, but this time selecting a "table" instead of a "form". Following the look of the original Style Card, we only included the AccessoryType and Description as shown in Figure 7 . We used the UI Editor to set the BorderLayout layout manager, to add an extra JPanel to the "South" of the panel, and created two JButton's in that panel for the (Insert) and (Delete) functionality. The wizard already took care of the data binding for the JTable, so we only needed to bind the two buttons to the appropriate action bindings. By selecting each of the two buttons in turn in the UI Editor, and clicking to edit their model property in the Property Inspector, we launch the "JClient Action Binding" editor where we select which view object to bind to ( StyleAccessoriesDescriptions) and which action the button should be "wired" to. The (Insert) button is wired this way to the "Create a new row" action, and the (Delete) button is wired to the "Delete current row" action for this view object.

PanelStyleAccessoriesDescriptions
Figure 7: PanelStyleAccessoriesDescriptions

Building the PanelGroups Panel

Next we use the "Panel" wizard again to create a form display for the StyleGroups view object, indicating to only include the one field for StyleGroupName. In this way, the wizard sets me up a prompt and a field for the group name. I deleted the field just created by the wizard, then clicked out two JComboBox controls into the GridBagLayout as shown in Figure 8 . Again I use the Property Inspector to edit the model property of the two comboxes to declaratively setup the navigation binding for the StyleGroups and StyleSubgroups, indicating to display the StyleGroupName and the StyleSubgroupName in the poplist respectively. I also added an extra JLabel to be the prompt for the 2nd "subgroup" poplist.

Since the navigation binding will automatically set the current row in the StyleGroups view object when the user picks a choice from the list, and since the StyleGroups and StyleSubgroups view objects are view linked, no user-written code is required to keep these datasets and poplists coordinated.

PanelGroups
Figure 8: PanelGroups

To create a prompt for the subgroup poplist, I switched quickly to the Code Editor and borrowed one line of code that the wizard generated for me to setup the "Group" prompt, and pasted a 2nd copy like this:

jLabel1.setText(panelBinding.getLabel("StyleSubgroups", "StyleSubgroupName", null));

This will cause the prompt for the subgroup poplist to be automatically picked up from the control hints supplied in the BC4J components for the StyleSubgroupName attribute instead of hard-coding a prompt in here.

Building the PanelLabels Panel

Last but not least, I created the PanelLabels panel using the JClient "Panel" wizard to build a Form-style panel for the StyleRecords view object, picking on the four label id fields that I added to the view above. Then, I deleted the default text fields created, and created four JComboBox controls in their place.

PanelLabels
Figure 9: PanelLabels

Again using the declarative binding editors that appear when I edit the model property of these controls, I setup their LOV Bindings. Figure 10 shows one of the two tabs of the model property editor for a combo box. I've defined the LOV binding to show choices in the list from the SizeLabelsLOV view object, and to use the StyleRecords view object as the target view to update when the user selects something from the list. Clicking (Add) to add an < LOVAttribute, TargetVOAttribute> pair, I pick the attribute names from the poplists in the table to that the LabelsId attribute from the row the user picks in the SizeLabelsLOV view object will automatically update the SizeLabelId attribute of the current row in the StyleRecords view object. This attribute binding information is also used to automatically coordinate the combobox to reflect the current value queried from the database for existing records as well.

Using LOV Binding Editor to Setup SizeLabels ComboBox
Figure 10: Using LOV Binding Editor to Setup SizeLabels ComboBox

I repeat this process three more times for the other three combo boxes, choosing their respective LOV view objects, and using the corresponding attribute of the StyleRecords view object as the target. With these bindings in place (coupled with the modified StyleRecords SQL query and the StyleRecordRowImpl.java server-side code we highlighted above) all of the functionality related to the "Labels" panel poplists is reduced to zero code. In particular, zero client-side code.

To finish the job for this panel, I created a JTable and set its model property to use the AttributeList table binding for tables to bind it to the OtherLabelsDescriptions view object. I added an extra JPanel to the bottom and put down two JButton instances and setup their properties and ActionBindings so that they would automatically be "wired" to the create and remove actions of the iterator.

Assembling the StyleFormApp Form From DataPanels

The last step is "assembling" the StyleApp Form from the constituent panels that we've created above. There is an easy way, and a hard way to do this. The easy way is the way which requires no hand-coding and which produces the most optimized code. As we've seen above, the "optimal" way often involves leveraging built-in features of the framework and/or framework design time.

In this case, I use the JClient "Empty Form" wizard to create a blank form. Then I use the property inspector and give it a title of "Style Card" using the property inspector.

Figure 11 shows the component palette set to the "JClient Controls" page, and illustrates which palette tool is for the "dataPanel" control. Clicking this "dataPanel" tool, and clicking into the Structure Pane or directly onto the UI Editor to select a parent container for the new panel, you will see a dialog that allows you to choose which JClient data panel you would like to place there. Only the classes in the current project that implement the oracle.jbo.uicli.controls.JClientPanel interface will appear in the list for selection.

The Handy JClient DataPanel Tool
Figure 11: The Handy JClient DataPanel Tool

The data panel tool handles all of the code generation in your form class to:

  1. Create a private member variable to hold an instance of the panel
  2. Instantiate the panel
  3. Set the panel instance's panelBinding to share the same panel binding that the form is using.

I used the UI Editor and the Data Panel tool to add an instance of each of our four JClient panels described above to produce the final StylesAppForm that you see in Figure 12

The Rebuilt StylesAppForm
Figure 12: The Rebuilt StylesAppForm

Implementing Additional Network Optimizations

This section describes the additional optimizations we made to further reduce network traffic.

Using Custom Application Module Methods to Setup Data Model

Application Modules are your business service components that give you a convenient place to "hang" code that does not belong in the:

  • entity objects

    That's where reusable business rules go

  • view objects

    That's where query-related logic goes, as well as customized, server-side row-handling logic and calculated attributes go.

  • client user interface layer

    That's where only the minimum amount of UI-specific presentation logic should go.

When working with JClient/Swing forms, think of the application module as the "server-side buddy" of your thin-client form. Whenever and wherever possible, code that works with your data model should be encapsulated inside an application module class and exposed as necessary a custom method for invocation by clients. This is especially true if you need to perform looping operations, data setup, or multiple BC4J API calls on view objects in your data model. By partitioning this code on the business service side of your three-tier application -- instead of in the client -- you can accomplish a large amount of work in a single network round trip.

Exposing a Custom Application Module Method

Before looking at specifically how we used a custom method in the customer's application to improve network round-trips, let's first briefly recall the steps to expose a custom method for invocation by a client. There are basically just two steps:

  1. Write a method in your MyAppModuleImpl.java class

    The method can use any serializable type as arguments and/or return value. You can also return any of the oracle.jbo.* interfaces like RowSet, Row, ViewObject, ApplicationModule, or arrays of those.

  2. Visit the Client Methods panel of the Application Module Editor as shown in Figure 13 , and shuttle the desired custom methods from the Available list to the Selected list.


    NOTE: Methods with return types or arguments of types which are not serializable will not show up in the list.
Selecting the Set of Custom App Module Methods to Expose
Figure 13: Selecting the Set of Custom App Module Methods to Expose

After clicking (OK) JDeveloper will generate a service interface for you which extends oracle.jbo.ApplicationModule and adds your custom methods that are exposed like this. By convention, if your service lives in a package a.b.c, then the custom service interface will live in package a.b.c.common.

package customer.app.model.services.common;
import oracle.jbo.ApplicationModule;
import oracle.jbo.domain.Number;
/**
 * Generated interface. Do not modify.
 */
public interface StylesApp extends ApplicationModule  {
  void setCollectionIdForStylesEditing(Number collectionId);
}

As we learned above, custom application module methods are even more powerful because of BC4J's Value Messenger design pattern implementation. Any data that their middle-tier logic causes to change or be refreshed will automatically be carried down to the thin client as part of the network round-trip response. We'll see how that helps us reduce round trips in the next section.

Writing the Custom AM Method to Setup the Data Model

In the original application given to us by Oracle Consulting, the form supplied a fixed "Collection ID" and "Department ID" as startup parameters in order to know which style information to edit. By studying the database diagram of the schema, it appeared that each collection had a unique ID and that each collection was related to exactly one department. So, I decided that it should be enough to pass in just the collection ID as the external information to use to "prime" the data model for editing the style information for that collection and the department that it's related to.

I started by modifying the query for the Departments view object to be a join between DEPARTMENTS and COLLECTIONS and to have a bind variable which allows me to restrict the result to a particular collection based on its ID. The query ended up looking like this:

SELECT Departments.DEPARTMENTS_ID, 
       Departments.DEPARTMENTS_ID_PRIOR, 
       Departments.DEPARTMENT_NAME, 
       Departments.SHORT_NAME, 
       Departments.GENDER, 
       Departments.ADMIN_FUNCTION, 
       Departments.DELETED, 
       Collections.Collections_ID AS COLLECTIONS_ID
FROM DEPARTMENTS Departments,
     COLLECTIONS Collections
WHERE Collections.DEPARTMENTS_ID = Departments.DEPARTMENTS_ID
  AND Collections.COLLECTIONS_ID = :1

By including the COLLECTIONS_ID as a select-list item in the query, this makes it easy to build View Links to other view objects later where the master/detail link might need the collection id.

With the query in place sporting our new binding variable for the collection ID, I wrote the setCollectionIdForStylesEditing method in the StylesAppImpl.java class that you see below, and published it as a custom method as described in the previous section. The method simply:

 

  • Sets the value of the first bind variable in the query (zero-based!) to the collection ID parameter value passed into the method,
  • Executes the query, and
  • Accesses the first row of the result.

 

  /*
   * Setup the driving department view object by getting the
   * appropriate collection ID to edit from the client. By performing
   * the query in the middle tier, we generate no additional client-server
   * round trips, and the view links automatically coordinate all of
   * the respective detail views also without any additional round trips.
   */
  public void setCollectionIdForStylesEditing(Number collectionId ) {
    getDepartments().setWhereClauseParam(0,collectionId);
    getDepartments().executeQuery();
    Row result = getDepartments().first();
  }

Since we have designed the data model in the StyleApp application module that has view link instances linking the master/detail view object instances, any time the current row changes in the iterator for the Departments view object, all related detail queries will be pro-actively executed to retrieve the related data. The line of code in our custom method above that calls the first() method on the view object causes the default iterator to move to that first row. In the process, all of the view linked view object queries are executed automatically.

The simple bit of code above, in combination with the default behavior of the view links, and the Value Messenger implementation, does the job of preparing the entire data model for the desired collection ID, related department, and all detail queries, as well as bringing all of that changed data back in bulk to the thin client cache in the same round trip.

In essence, what we're saying is that when the Java VM in the thin client executes the call to this remote custom AM method like this:

/*
 * Before the call, there is no data yet in the BC4J thin client cache.
 */
 stylesApp.setCollectionIdForStylesEditing(new Number(87065) /* Collection ID */);
/*
 * After the method call, the Value Messenger implementation has returned in bulk
 * all of the data retrieved as a result of the custom method invocation.
 */

This means that the data for the StyleRecords view object, and all other view objects that our JClient bindings are "bound" to are now in the thin-client cache. All of this happens in a single round-trip to the middle tier, and as we've now appreciated, all without writing any "plumbing" code ourselves to perform this coordination or bulk data-transfer.

Calling the Custom AM Method from the Client Form

The small amount of code that we've written in the client is clearly identified in the StyleAppForm.java class by the comments:

/*
 * ---v----v----v---v--- START OF ADDED CODE ---v----v----v---v---
 */

and

/*
 * ---^----^----^---^--- END OF ADDED CODE ---^----^----^---^---
 */

The first bit of code that we'll highlight is the code that invokes the custom AM method we created above, and which causes all of the data needed by the form at startup time to be retrieved in that single round trip. Earlier in the code we retrieved the application module from the panel binding, and here we cast it to the custom StylesApp interface so we can call our custom method.


NOTE:

Since we were only supplied the data for a single collection to test with (#87065), we have hard-coded the collection ID that is passed into the custom method.


      /*
       * Cast the appmodule to our custom StylesApp module interface
       * so we can invoke the custom application module method to
       * setup the datamodel needed for this form. In this case, we're
       * passing in all arguments that will be needed in order to
       * properly setup the data model. All of the data model setup
       * occurs on the server side, so we don't end up having lots
       * of thin-client-to-appserver roundtrips to set it up.
       */
      StylesApp stylesApp = (StylesApp)appMod;

      /*
       * The real application would get this collection ID to edit
       * from some other context... This is just for testing.
       */
      final Number COLLECTION_ID_TO_EDIT = new Number(87065);
      
      stylesApp.setCollectionIdForStylesEditing(COLLECTION_ID_TO_EDIT);

      /*
       * No need to do the panelBinding.execute() since we already
       * performed the execute on the server.
       */
       //   panelBinding.execute();
      
      /*
       * Since when we cancel we are bailing out of the window via
       * window close or pressing the (Cancel) button, there is no
       * value in re-executing the iterators in the panel binding.
       */
      panelBinding.setExecuteOnRollback(false);

Since all of the data has been retrieved by the few lines of code in our custom AM method, we have commented out the call to the execute() method on the panelBinding that the wizard generated into the form. Executing this method again would cause additional, unnecessary round trips. It's generated there by default to make sure that any panel or form will have data when it appears without further user coding, but here we're trying to make things work in the most optimal way.

Also, since the (Cancel) button on the form presumably will cause the form to exit, there is no value provided by the default behavior of re-executing the panelBinding upon a rollback of the transaction. Therefore, to save this step we've called the setExecuteOnRollback(false).

Fetching the Attribute Properties in Bulk

One of the powerful features of the BC4J framework is the ability to access view object and view object attribute metadata in your client application.

What are Attribute Properties?

This feature makes it possible to build intelligent, metadata-driven behavior in the client to avoid hard-coding. One built-in example that's illustrated in the rebuilt sample application is the use of attribute "Control Hints", like Tooltip and Label. Figure 14 shows the panel of the View Object Editor in JDeveloper 9.0.3.3 where control hints can be set. JDeveloper manages saving your translatable tips into a Java message bundle, allowing you to easily translate the bundles into multiple languages.

Setting Attribute-Level Control Hints Like Label and Tooltip
Figure 14: Setting Attribute-Level Control Hints Like Label and Tooltip

At runtime, your application can access the control hints as part of constructing the end-user UI controls. For example, the following four lines of code in the PanelStyleGroups.java class setup the label and tooltips for the two comboboxes based on the control hints that are accessible from the JClient panel binding object:

// Setup label and tooltip for style group name JLabel and JComboBox
labelStyleGroupName.setText(
  panelBinding.getLabel("StyleGroups", "StyleGroupName", null)
);
styleGroupNameComboBox.setToolTipText(
  panelBinding.getTooltip("StyleGroups", "StyleGroupName", null)
);
// Setup label and tooltip for style subgroup name JLabel and JComboBox
jLabel1.setText(
  panelBinding.getLabel("StyleSubgroups", "StyleSubgroupName", null)
);
jComboBox1.setToolTipText(
  panelBinding.getTooltip("StyleSubgroups", "StyleSubgroupName", null)
);

Figure 15 shows what the control-hint driven labels and tooltips look like at runtime.

Control Labels and Tooltips Driven from BC4J Control Hints
Figure 15: Control Labels and Tooltips Driven from BC4J Control Hints

Optimizing the Retrieval of Metadata and Control Hints

The ApplicationModule supports the fetchAttributeProperties method that allows your JClient application to fetch all view object metadata and attribute properties in a single network trip. To get this optimization, we added the following code in the StyleFormApp.java class:

      /*
       * Array of view object instance names for which to fetch attribute props
       */
      String[] voNames = new String[]{
        "StyleRecords",
        "StyleAccessoriesDescriptions",
        "StyleGroups",
        "StyleSubgroups",
        "SizeLabelsLOV",
        "HangLabelsLOV",
        "BarcodeLabelsLOV",
        "MainLabelsLOV",
        "OtherLabelsDescriptions"
      };

      /*
       * Fetch all of the attribute properties in one round trip
       */
      ApplicationModule appMod = app.getApplicationModule();
      appMod.fetchAttributeProperties(voNames,null,null);

Pinning the ViewObjects We're Working With

The view objects being used by the BC4J thin-client cache are currently held in a WeakVector. We use a WeakVector so that view objects that are no longer being actively used by the client can be garbage-collected by the client Java VM if memory becomes scarce. If one of the view objects gets garbage-collected from the client cache, and later is accessed again, its metadata will be re-fetched on demand from the middle-tier business service. As a domino effect, if the view object is garbage-collected, then all of the client-side metadata related to it will also be garbage-collected and might need to be fetched again on demand from the middle tier. No problem, except that this can cause additional network round trips. You can avoid these unexpected roundtrips by building some additional application-specific knowledge into your client. For example, by "pinning" the view objects that your form is using in a local member array they won't be subject to garbage-collection as long as your form is still running. This will insure that the metadata, which we fetched in a single round-trip with the fetchAttributeProperties method above, will not ever need to be re-fetched on-demand due to client-side garbage collection. It may mean that your client application uses a little more memory than it might otherwise use, but the tradeoff is fewer network roundtrips.

In the StyleAppForm.java class, you will find the additional code below which simply creates an array of ViewObject interfaces, and caches these interfaces in a member array.

      /*
       * Cache ViewObjects so that WeakVector garbage collection
       * does not cause client to "forget" about these views, which
       * would cause additional round trips to re-fetch the information
       * about the view objects, as well as the attribute-property information
       */
      ViewObject[] vos = new ViewObject[voNames.length]; 
      for (int i = 0; i < vos.length; i++) { 
        vos[i] = appMod.findViewObject(voNames[i]); 
      }

Pre-Creating Default RowSet and RowSetIterators

The last technique we've used to further reduce network traffic for application startup in the rewritten sample is the eager, pre-creation of the default RowSet and RowSetIterator for all of the view objects that our form will be using.


NOTE:

This web log article explains the relationship between a view object and its default rowset, and between a rowset and its default iterator.


By overriding the create() method in our StyleAppImpl.java application module class, we can set the range size for all of the view objects that we know will be fetching all rows. The act of setting the range size will cause the default RowSet and its default RowSetIterator to be created ahead of time. This way, the client will not have to do network roundtrips to create them later.

The server-side application module code looks like this. Recall that setting the range size to -1 means that the range will contain all rows.

  /*
   * Preset the range sizes used by this panel
   * This will indirectly cause the default
   * rowset and default rowsetiterator to be created.
   */
  protected void create()  { 
    super.create(); 
    //lovs are set to rangeSize of -1. 
    getBarcodeLabelsLOV().setRangeSize(-1); 
    getHangLabelsLOV().setRangeSize(-1); 
    getMainLabelsLOV().setRangeSize(-1); 
    getOtherLabelsDescriptions().setRangeSize(-1); 
    getSizeLabelsLOV().setRangeSize(-1); 
    getStyleGroups().setRangeSize(-1); 
    getStyleSubgroups().setRangeSize(-1); 
    getStyleRecords().setRangeSize(-1); 
    getStyleAccessoriesDescriptions().setRangeSize(-1); 
  } 

Network Performance of Rewritten Sample

Setting Up the Test Data Environment

The DatabaseSetup directory in the OptimizedRoundtrips.zip file contains four SQL scripts. Three of these were provided by Oracle Consulting:

  1. CreateTabels.sql
  2. DataToTables.sql
  3. Data.sql

As part of our testing, we slightly modified the sample data we were provided so that some rows would be retrieved by every query. Specifically, we made the small modifications contained in the fourth SQL script that is also in this same DatabaseSetup directory named FixData.sql.

By creating a new MC_TEST database user, and running the above four SQL scripts in order, you will have the same data environment that we used for the final network traffic measurements below.

Our Final Network Round Trip Measurements

Using all of the techniques documented above, we were able to reduce the network round trips at startup for the "Style Card" form from well over 100, to just 15 round trips using JDeveloper 9.0.3.3.

Table 1 documents the 15 round trips and what they are doing...

Table 1: Network Roundtrips at Startup for Rewritten Example
Round Trips Remote Method Calls Description
6
  • isConnected (2)
  • connectToDataSource (1)
  • getConnectionMetadata (2)
  • prepareSession (1)
Sets up the root application module and its database connection. This is a one-time root AM startup cost.
1 getLocale Syncs up the client and server locale for the user
1 fetchAttributeProperties Bulk-fetches all view object metadata and attribute properties
3 syncIterator (3) Syncs each updatable iterator with its middle-tier counterpart
2 syncIterator (2) Each navigation binding calls syncIterator to set the currency on the first row.
2 getEstimatedRowCount (2) Retrieves estimated row count needed by table binding to properly size any eventual table scroll bar

This optimal result occurs when using the latest JDeveloper 9.0.3.3 maintenance release or later and all of the view objects have at least one row of data in their result set.

Due to some Swing JScrollPanel issues, when zero rows are returned in a data source, two additional client-side methods can occur while the JTable binding is being asked to participate in calculating the number of rows that can "fit" in the table. Table 2 describes these additional round trips that might appear in your application where no data is queried.

Table 2: Additional Remote Method Calls Related to Table Bindings
Remote Method Call Description
setRangeSize The Swing JScrollPanel tries to calculate the size of the scroll panel's scroll bar based on the number of rows that will fit in the display area. The table binding then adjusts the range size to match that number of rows that can display.
scroll In JDK 1.3, the Swing JScrollPanel generates an extra scroll event when there is no data in the table. (Could likely be optimized out for 9.0.3.4 maintenance release). Doesn't appear to happen any more for JDK 1.4.

NOTE:

While this paper focuses on tips to apply for the 9.0.3.X series of JDeveloper and BC4J, it's interesting to note that we have been working hard over the last year and a half to further optimize the thin-client network traffic. We have introduced a new "Batch Mode" in JDeveloper 10g and the versions of the JClient and BC4J frameworks that accompany it in order to further consolidate what number and types of operations we are able to perform in a bundled way as part of a single network round-trip.


Tips for Quicker Development

While working on this sample application, I used one design-time trick to speed up my development/test/profile cycle that is worth sharing. When you create a BC4J Deployment Profile for deploying your application module as an EJB Session Bean, the BC4J Design Time extension creates you two additional application module configurations: one for remote EJB access, and one for accessing the EJB Session Bean inside the embedded OC4J container in JDeveloper. For example, for the StyleApp application module, these two extra configurations are named StylesApp9iAS and StylesAppBM9iASEmbedded respectively.

Normally you can click on the *.cpx file in your view project, like the View.cpx file in our View.jpr project for the rewritten sample, and see the client data model definitions that it contains in the Structure Window. By selecting one of these client data models and selecting the Modify... from the context menu, you can easily change the configuration that your JClient form will use at startup to test in local mode (2-Tier) or remote mode (3-Tier).

In JDeveloper 9.0.3, the configuration name for the embedded OC4J deployment is unfortunately not present in the poplist to choose from. However, with a quick edit of the View.cpx file outside of JDeveloper, we can set the name of the Configuration attribute of the <Session> element to have the value " StylesAppBM9iASEmbedded" as shown below.

<JboProject
   Name="View"
   Package="" >
   <Session
      Name="StylesApp"
      Package="customer.app.model.services"
      Configuration="StylesAppBM9iASEmbedded" >
   </Session>
</JboProject>

Then next time you load the project in JDeveloper and run your form from inside the IDE with the *.cpx file changed this way, you can run, debug, or profile your 3-tier application without having to go through the physical deployment step to an external application server each time the middle-tier project undergoes any changes.

To run your EJB Session Bean, simply find it the navigator as shown in Figure 16 and run or debug it.

Running the EJB Session Bean for Your AM on the Embedded Container
Figure 16: Running the EJB Session Bean for Your AM on the Embedded Container

With the server-side of your 3-tier application running in the embedded OC4J container inside JDeveloper, and with the SalesApp client data model inside your View.cpx file pointing at the StylesAppBM9iASEmbedded configuration, you can then Run, Debug, or Profile your client application without the hassle of deployment each time. If you make changes to your middle-tier components, just stop the EJB in the embedded container using JDeveloper's Run | Terminate... menu, then re-run it.

JDeveloper even allows you to run, debug, and/or profile multiple applications at once so you can pick any combination of running, debugging, or profiling either or both of your thin-client and middle tier components.

When you're ready to do the real deployment, deploy to the external application server, change your client data model to use the SalesApp9iAS configuration again, and re-run the form.

Other Issues

We close with a few brief notes on other observations that we've made on the initial application that was supplied to us:

  • Optimize View Object Fetch Sizes to Reduce AppServer-to-Database Round Trips

    The "Fetch Size" parameter on the "Tuning" panel of the View Object editor controls how many rows at a time should be fetched via JDBC from the database to the application server where the BC4J middle-tier components are running. By default, view objects of a fetch size of one. If you write a query that you know is being used to populate a list of values, then set the fetch size to a larger number to make fewer round trips to the database to grab the data. If you leave it at the default of 1, you'll make as many database round trips as you have rows to fetch. While this will not affect the thin-client-to-appserver round trips, it does incur a performance penalty for the extra round-trips between application server and database.

  • Minimize Database Connections by Using Root AppModules Correctly

    Each time you create a top-level JUApplication object in JClient, it will correspondingly create a top-level (or "root") application module that will have its own database connection. Sometimes you want this when different JClient forms need to have completely independent transactions (that is, a rollback in one does not affect pending work in another). However, BC4J and JClient were carefully design to work together and BC4J's feature to support nested application module instances is paralleled by JClient's ability to create nested JUApplication objects. By leveraging nested application modules and nested JUApplications you can decide which forms should share a transaction as well as making it more easy to load and dispose of application modules as the forms that use them are dismissed. It requires some work on the developer's part to exploit these framework features, but as we've seen above, the payback can be worth the effort in manageability and performance.

    Also, we've seen in the document here that by design a JClient form should be assembled of instances of JClient panels (except for the simplest of forms that might only need a single form with no extra panels). These panels are designed to all share a single panel binding from the form that contains them. The JDeveloper UI Editor and JClient property editors cooperate to set this up for you automatically when you use the "Data Panel" tool from the "JClient Controls" component palette page.

  • Use bind variables

    Wherever possible, use bind variables in your view object SQL statements instead of concatenating in text values into the WHERE clause. This makes the database more efficient at reusing your statements.

  • Query Only the Rows and Columns That You Need

    In many of the original sample application's view objects, we observed many columns being queried which did not appear to be displayed on the user interface. The view objects exist to let you give a custom "shape" to the data you query, and allow you to only query the columns of data (and of course only the rows of data!) that you know your form will need do display.

  • Keep an Eye Out for Lazy Master/Detail Coordination Opportunities

    More sophisticated user interfaces might make use of Swing's tabs or card layouts to have a set of panels which are conditionally displayed (or displayed only when the user brings them to the foreground). While not automatic in the 9.0.3 release, BC4J does offer API's like setMasterRowSetIterator and removeMasterRowSetIterator on any RowSet which allow you to dynamically add and remove iterators from the list of ones that will cause that rowset to be actively coordinated by the framework. Using these API's in a clever way (where possible, from within a server-side AM custom method of course!) you can have you application automatically coordinate the detail queries for regions on the screen that the user can see, and suppress the active coordindation for data that the user cannot currently see on the screen.

Summary

This paper has highlighted a number of unique features of the Oracle BC4J and JClient frameworks that enable much simpler creation of business applications with rich-client Swing front ends. In the process of rebuilding the sample application, we studied several best-practices techniques for measuring and reducing network traffic between the thin-client display and the middle-tier application components.

false ,,,,,,,,,,,,,,,