This sample shows how to integrate a SmartClient front-end with Ruby on Rails (also known as RoR or just "Rails").
The content discussed here will explore a way to integrate RoR with SmartClient, by leveraging SmartClient javascript API's on the front-end and MVC architecture in RoR web application on the back-end to generate the result sets. In this sample, a DataSource will be created which inherits from RestDataSource. This will get the data from the database using the smartclient gem and will serialize it back to the DataSource in JSON format. The results will be displayed in a list grid.
Prerequisites
- Ruby on Rails framework, at least version 3.x (requires Ruby 1.8.6 or higher). It can be downloaded for free from here.
- MySQL Server has been used as the database.
- SmartClient SDK, at least version 8.3p. This can be downloaded from here. Any edition is OK, including the free LGPL version.
1. Setting up the application
Once you installed Rails on your development computer, first create the new Rails project with the command as follows:
rails new smartclient_app
After the new project has created, unzip your SmartClient SDK and copy the complete "isomorphic" folder from *smartclientRuntime\isomorphic to the public directory in your application's folder.
Next open the Gemfile in the project directory and add the 'smartclient' gem. You can download the gem from github. We will discuss how the smartclient gem was written at the end of this article.
The "smartclient version 0.0.7" supports the various helper classes and methods for the SmartClient framework, you can use it easily.
gem "smartclient", "~> 0.0.7" gem 'mysql2'
Then run the bundle command on the console:
bundle install
Configuring a Database
The database to use is specified in a configuration file, config/database.yml. If you open this file in a new Rails application, you’ll see a default database configured to use SQLite3. The file contains sections for three different environments in which Rails can run by default:
- The development environment is used on your development/local computer as you interact manually with the application.
- The test environment is used when running automated tests.
- The production environment is used when you deploy your application for the world to use.
If you choose to use MySQL database, your config/database.yml will look a little different. Here’s the development section:
development: adapter: mysql2 encoding: utf8 database: smartclient_development pool: 5 username: root password: socket: /tmp/mysql.sock |
If your development computer’s MySQL installation includes a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the development section as appropriate.
Creating database and sample SQL table
Now that you have your database configured, it’s time to have Rails create an empty database for you. You can do this by running a rake command:
rake db:create:all
And you need to define the schema.rb and create the table.
ActiveRecord::Schema.define(:version => 20130521133624) do create_table "employees", :primary_key => "EmployeeId", :force => true do |t| t.string "Name" t.string "ReportsTo" t.string "Job" t.string "Email" t.string "EmployeeType" t.string "EmployeeStatus" t.float "Salary" t.string "OrgUnit" t.string "Gender" t.string "MaritalStatus" t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end create_table "supplyitems", :primary_key => "itemID", :force => true do |t| t.string "itemName" t.string "SKU" t.string "description" t.string "category" t.string "units" t.float "unitCost" t.boolean "inStock" t.datetime "nextShipment" t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end end
Once you defined the table structures, you can run the rake command and then the new table will be created on the MySQL database.
rake db:schema:load
Generate sample data
In order to display the data on the list grid, the table that just was created should have sample rows.
You can do it by running a rake command, before you run the command, you need to define the seeds.rb in the db directory of the application.
# This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). # for count in 1..20 itemID = count itemName = "Pens stabiliner " + count.to_s SKU = Random.rand(1000...9999) category = "Office paper Product" units = "Ea" unitCost = Random.rand(0.1...2) inStock = 1 nextShipment = Time.now Supplyitem.create( :itemName => itemName, :SKU => SKU, :description => "Description" + itemID.to_s, :category => category, :units => units, :unitCost => unitCost, :inStock => inStock, :nextShipment => nextShipment ) Name = "Employee Name" + count.to_s ReportsTo = count.to_s Job = "Employee Job" + count.to_s Email = "employee" + count.to_s + "@gmail.com" EmployeeType = "EmployeeType" + count.to_s EmployeeStatus = "EmployeeStatus" + count.to_s Salary = "Salary" + count.to_s OrgUnit = "OrgUnit" + count.to_s Gender = "1" MaritalStatus = "1" Employee.create( :Name => Name, :ReportsTo => ReportsTo, :Job => Job, :Email => Email, :EmployeeType => EmployeeType, :EmployeeStatus => EmployeeStatus, :Salary => Salary, :OrgUnit => OrgUnit, :Gender => Gender, :MaritalStatus => MaritalStatus ) end
And you can run the command as follows;
rake db:seed
Finally you can start implementing with smartclient.
2. Create two Models
rrails g model supplyitem rails g model employee
class Supplyitem < ActiveRecord::Base attr_accessible :itemID, :itemName, :SKU, :description, :category, :units, :unitCost, :inStock, :nextShipment end
class Employee < ActiveRecord::Base attr_accessible :EmployeeId, :Name, :ReportsTo, :Job, :Email, :EmployeeType, :EmployeeStatus, :Salary, :OrgUnit, :Gender, :MaritalStatus end
3. Adding SmartClient library path to the layout view
When Rails renders a view as a response, it does so by combining the view with the current layout.
We need to add the SmartClient library path to the layout page.
<!DOCTYPE html> <html> <head> <title>Smartclient</title> <%= stylesheet_link_tag "application", :media => "all" %> <script>var isomorphicDir = "./isomorphic/";</script> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> </head> <body> <%= yield %> </body> </html>
//= require ./isomorphic/system/modules/ISC_Core //= require ./isomorphic/system/modules/ISC_Foundation //= require ./isomorphic/system/modules/ISC_Containers //= require ./isomorphic/system/modules/ISC_Grids //= require ./isomorphic/system/modules/ISC_Forms //= require ./isomorphic/system/modules/ISC_DataBinding //= require ./isomorphic/skins/TreeFrog/load_skin
Adding these script tags will now ensure the master page will manage the loading of SmartClient, providing that this page is always defined as the master page for the view.
4. Creating the DataSource and Rails controller for data requests
We can use the rails command to create the new controller.
rails g controller smartclient
The Rails router recognizes URLs and dispatches them to a controller’s action. It can also generate paths and URLs, avoiding the need to hardcode strings in your views.
So after you created the new controller, you should define the routes.rb
Smartclient::Application.routes.draw do root to: 'smartclient#index' get 'smartclient/data' end
By running rails server command, the application will start and a grid fetching and displaying the rows found in the table will be shown. (http://localhost:3000)
rails s
Now we are ready everything! Once we define the user interface javascript with smartclient as the JSON format, we can implement and control any component of the smartclient with the gem through the controller class.
5. Handling the SmartClient data with the gem
1) Handling a Fetch Request From RestDataSource
Open the created view for the application (in the Solution Explorer, in the app/views/smartclient folder, open Index.html.erb) and change it's content to:
<SCRIPT> isc.RestDataSource.create({ "ID": "suppyItem", "fields":[ {"name":"itemID", "type":"sequence", "hidden":"true", "primaryKey":"true"}, {"name":"itemName", "type":"text", "title":"Item", "length":"128", "required":"true"}, {"name":"SKU", "type":"text", "title":"SKU", "length":"10", "required":"true"}, {"name":"description", "type":"text", "title":"Description", "length":"2000"}, {"name":"category", "type":"text", "title":"Category", "length":"128", "required":"true" }, {"name":"units", "type":"enum", "title":"Units", "length":"5", "valueMap":["Roll", "Ea", "Pkt", "Set", "Tube", "Pad", "Ream", "Tin", "Bag", "Ctn", "Box"] }, {"name":"unitCost", "type":"float", "title":"Unit Cost", "required":"true", "validators":[ {"type":"floatRange", "min":"0", "errorMessage":"Please enter a valid (positive) cost"}, {"type":"floatPrecision", "precision":"2", "errorMessage":"The maximum allowed precision is 2"} ] }, {"name":"inStock", "type":"boolean", "title":"In Stock"}, {"name":"nextShipment", "type":"date", "title":"Next Shipment"} ], "dataFormat":"json", "dataURL":"/smartclient/data" }); isc.ListGrid.create({ ID: "suppyItem", width: 700, height: 224, alternateRecordStyles: true, dataSource: suppyItem, autoFetchData: true }); </SCRIPT>
Create a datasource which inherits RestDataSource and define the data URL to a controller. This controller will handle the requests from the DataSource. The fields in the newly created DataSource will match the columns in the database table created earlier.
Note, The required format is 'json' and the data url is set to 'smartclient/data', which means the controller name will need to be set to 'SmartclientController' and the method name will be 'fetch'. As everything else is unchanged, the DataSource will send the request parameters in the URL of the request.
Now, describe the controller to send back the response. You need to include the DSResponse.rb file in the header, the DSResponse class defines the response structure.
require 'DSResponse' class SmartclientController < ApplicationController def index end def data # get all supplyitems from the database @supplyItems = Supplyitem.find(:all) # get the count of the supplyitems supplyitems_count = Supplyitem.count response = DSResponse.new response.data = @supplyItems response.startRow = 0 response.endRow = supplyitems_count - 1 response.status = 0 response.totalRow = supplyitems_count @result = { :response => response } render json: @result end end
2) Adding simple Criteria, Sort, and Data Paging
Firstly, Change the DataSource so, instead of using the URL parameters for sending data to the back-end (filter, etc), send parameters as a JSON payload to the POST method. Additionally, configure the list grid to let the back-end do the pagination and filtering.
For this, remove the 'dataFetchMode:"local"' attribute on the grid. This will cause the grid to send criteria and pagination information to the back-end:
isc.ListGrid.create({ ID: "supplyItem", width: 700, height: 224, alternateRecordStyles: true, dataSource: supplyItem, showFilterEditor: true, autoFetchData: true, dataPageSize: 20 });
Please note:, Grid filters have also been enabled with showFilterEditor:true. This allows for easier testing later. Also note that the dataPageSize has been set to 20, to show pagination working.
Additionally, change the DataSource definition to use POST instead of the default GET HTTP method used by normal fetch operations. This will post the JSON as a payload to the server instead of passing it the variables in a URL. To do this, add an operationBinding definition to the DataSource. The DataSource should now look like this:
isc.RestDataSource.create({ "ID": "supplyItem", "fields":[ {"name":"itemID", "type":"sequence", "hidden":"true", "primaryKey":"true"}, {"name":"itemName", "type":"text", "title":"Item", "length":"128", "required":"true"}, {"name":"SKU", "type":"text", "title":"SKU", "length":"10", "required":"true"}, {"name":"description", "type":"text", "title":"Description", "length":"2000"}, {"name":"category", "type":"text", "title":"Category", "length":"128", "required":"true" }, {"name":"units", "type":"enum", "title":"Units", "length":"5", "valueMap":["Roll", "Ea", "Pkt", "Set", "Tube", "Pad", "Ream", "Tin", "Bag", "Ctn", "Box"] }, {"name":"unitCost", "type":"float", "title":"Unit Cost", "required":"true", "validators":[ {"type":"floatRange", "min":"0", "errorMessage":"Please enter a valid (positive) cost"}, {"type":"floatPrecision", "precision":"2", "errorMessage":"The maximum allowed precision is 2"} ] }, {"name":"inStock", "type":"boolean", "title":"In Stock"}, {"name":"nextShipment", "type":"date", "title":"Next Shipment"} ], "dataFormat":"json", "operationBindings": [ { "operationType": "fetch", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" }, ] });
If the sample,was run at this stage the payload sent to the server will look similar to this:
{ "dataSource":"suppyItem", "operationType":"fetch", "startRow":0, "endRow":20, "textMatchStyle":"substring", "componentId":"suppyItem", "data":{ "units":"Pkt", "unitCost":"a" ... }, "oldValues":null }
The "smartclient" gem supports the DSRequest class to define the object which is created by de-serializing from JSON and which mimics the previous JSON.
require 'RPCManager' class SmartclientController < ApplicationController def index end def data request = params[:smartclient] rpc = RPCManager.new(request) rpc.model = Supplyitem @result = rpc.processRequest render json: @result end end
The RPCManager helper class which the smartclient gem supports can process the request. When the new object of the RPCManager class parses request and model, we need to set the model to the RPCManager object. After the new object of the RPCManager was created, if you call the processRequest, the gem will process the request.
The buildStandardCriteria method of the DataSource helper class is used when the user defines the filter for the front-end.
The complete code for this sample project can be downloaded from github.
3) Addding other CRUD operations
In this part the code will be refactored from the previous sample (Adding simple criteria, sorting and data paging), the correct filtering functions will be implemented for the text columns and additional functionality will be added to have a fully working DataSource implementation.
In the previous sample, filtering by a text value would not select rows which contained the filter term, only those that were specifically equal to it. To correctly filter by a text value, it is necessary to identify which columns are of type text and which are other types. To achieve this, the declaration of the DataSource needs to be moved to a separate file, which will be loaded on both the client and server side.
Firstly, create a new folder, called 'datasource' in the /app/assets/javascripts directory. In this folder, create a javascript file which defines the DataSource itself. Give this file the same name as the ID of the DataSource.
isc.RestDataSource.create({ "ID": "suppyItem", "fields":[ {"name":"itemID", "type":"sequence", "hidden":"true", "primaryKey":"true"}, {"name":"itemName", "type":"text", "title":"Item", "length":"128", "required":"true"}, {"name":"SKU", "type":"text", "title":"SKU", "length":"10", "required":"true"}, {"name":"description", "type":"text", "title":"Description", "length":"2000"}, {"name":"category", "type":"text", "title":"Category", "length":"128", "required":"true" }, {"name":"units", "type":"enum", "title":"Units", "length":"5", "valueMap":["Roll", "Ea", "Pkt", "Set", "Tube", "Pad", "Ream", "Tin", "Bag", "Ctn", "Box"] }, {"name":"unitCost", "type":"float", "title":"Unit Cost", "validators":[ {"type":"floatRange", "min":"0", "errorMessage":"Please enter a valid (positive) cost"}, {"type":"floatPrecision", "precision":"2", "errorMessage":"The maximum allowed precision is 2"} ] }, {"name":"inStock", "type":"boolean", "title":"In Stock"}, {"name":"nextShipment", "type":"date", "title":"Next Shipment"} ], "criteriaPolicy":"dropOnChange", "dataFormat":"json", "operationBindings": [ { "operationType": "fetch", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" }, { "operationType": "add", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" }, { "operationType": "update", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" }, { "operationType": "remove", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" } ] });
Now we should change the user interface for the CRUD operations with the smartclient.
isc.ListGrid.create({ ID: "supplyItem", width: 700, height: 224, alternateRecordStyles: true, dataSource: supplyItem, showFilterEditor: true, autoFetchData: true, dataPageSize: 20, canEdit:true, canRemoveRecords:true }); isc.IButton.create({ top: 250, title: "Edit New", click: "suppylItem.startEditingNew()" });
Now, modify the view file for the application to load this additional DataSource. You should edit the 'app/views/smartclient/index.html' file and change it's content to:
<%= javascript_include_tag "datasource/supplyitem" %> <%= javascript_include_tag "smartclient_ui" %>
We don't need to define the controller again.
The add, fetch, remove, update methods of the DataSource helper class of the gem is used for the CRUD through the RPCManager helper class.
The complete code for this sample project can be downloaded from github.
4) Add queuing and transaction support
SmartClient has advanced features for queuing multiple requests in one single request. This provides a mechanism for sending multiple requests to the server in a single HTTP turnaround, thus minimizing network traffic as well as allowing the server to treat multiple requests as a single transaction (if the server is able to do so).
You need to add the save button for the transaction progress.
isc.ListGrid.create({ ID: "supplyItemGrid", width: 700, height: 224, alternateRecordStyles: true, dataSource: supplyItem, showFilterEditor: true, autoFetchData:true, dataPageSize:20, canEdit:true, canRemoveRecords:true, autoSaveEdits: false }); isc.IButton.create({ top: 250, title: "Edit New", click: "supplyItemGrid.startEditingNew()" }); isc.IButton.create({ top: 250, left: 100, title: "Save all", click: "supplyItemGrid.saveAllEdits()" });
We don't need to define the controller again.
The processTransaction method of the RPCManager is used for the transaction.
5) Adding support for AdvancedCriteria
In this part, we will describe how to modify the FilterBuilder and the underlying AdvancedCriteria system, to build functionality resembling this showcase sample (but using our supplyItem DataSource, instead of the one in the webpage)
Modify app/assets/javascripts/smartclient_ui.js to include the relevant code for creating the FilterBuilder:
isc.FilterBuilder.create({ "ID": "advancedFilter", "dataSource": "supplyItem", "topOperator": "and" });
The ListGrid also requires additional code to add the FilterBuilder. This will require adding a vertical layout (VStack), together with the grid and the button needed to add for applying the filter on the ListGrid. Also going to add a horizontal layout (HStack) which will contain the two already existing buttons used for saving all data and creating a new record:
isc.ListGrid.create({ ID: "supplyItemGrid", width: 700, height: 224, alternateRecordStyles: true, dataSource: supplyItem, autoFetchData:true, dataPageSize:20, canEdit:true, canRemoveRecords:true, autoSaveEdits: false }); isc.IButton.create({ ID: "filterButton", title: "Filter", click: function () { supplyItemGrid.filterData(advancedFilter.getCriteria()); } }); isc.HStack.create({ membersMargin: 10, ID: "gridButtons", members: [ isc.DynamicForm.create({ values: { dataSource: "Change DataSource" }, items: [ { name: "dataSource", showTitle: false, editorType: "select", valueMap: ["supplyItem", "employee"], change: function (form, item, value, oldValue) { if (!this.valueMap.contains(value)) return false; else { supplyItemGrid.setDataSource(value); advancedFilter.setDataSource(value); supplyItemGrid.filterData(advancedFilter.getCriteria()); } } } ] }), isc.IButton.create({ top: 250, title: "Edit New", click: "supplyItemGrid.startEditingNew()" }), isc.IButton.create({ top: 250, left: 100, title: "Save all", click: "supplyItemGrid.saveAllEdits()" }), ] }); isc.VStack.create({ membersMargin: 10, members: [advancedFilter, filterButton, supplyItemGrid, gridButtons] });
Also note, the filter has been removed top of the grid, as it is being replaced with the FilterBuilder.
The smartclient gem can parse the advanced criteria, if you take a look at the source code of the smartclient gem, you will find the fetch method in the DataSource.rb in the gem library direcotry, after you bundled the smartclient gem.
The AdvancedCriteria built by the FilterBuilder is sent in the JSON payload when doing a fetch() request. It is formatted like this:
// an AdvancedCriteria { "_constructor":"AdvancedCriteria", "operator":"and", "criteria":[ // this is a Criterion { "fieldName":"salary", "operator":"lessThan", "value":"80000" }, { "fieldName":"salary", "operator":"lessThan", "value":"80000" }, ... possibly more criterions .. { "operator":"or", "criteria":[ { "fieldName":"title", "operator":"iContains", "value":"Manager" }, { "fieldName":"reports", "operator":"notNull" } { "operator":"and", "criteria": [ .. some more criteria or criterion here ]} ]} }, { "operator":"or", "criteria":[ { "fieldName":"title", "operator":"iContains", "value":"Manager" }, { "fieldName":"reports", "operator":"notNull" } ]} .. possibly more criterions or criterias ] }
As you can see it is a tree structure, with it's leafs being criterion and the nodes being criteria. If the the criteria member is null, then it is a leaf, otherwise it is a node which has sub-criterias or sub-criterions.
The buildCriterion method is called for this part.
6) Make it data-driven
This example takes the previous sample and makes it data driven and adds a way for the user to define new DataSource types. It will also be extended to define a new DataSource and change the user interface to allow the user to switch between the two DataSources.
We already defined the "employees" table so that we need to add the datasource.
isc.RestDataSource.create({ "ID": "employee", "fields": [ { "name": "Name", "title": "Name", "type": "text", "length": "128" }, { "name": "EmployeeId", "title": "Employee ID", "type": "integer", "primaryKey": "true", "hidden": "true" }, { "name": "ReportsTo", "title": "Manager", "type": "integer", "required": "true", "rootValue": "1", "detail": "true" }, { "name": "Job", "title": "Title", "type": "text", "length": "128" }, { "name": "Email", "title": "Email", "type": "text", "length": "128" }, { "name": "EmployeeType", "title": "Employee Type","type": "text", "length": "40" }, { "name": "EmployeeStatus", "title": "Status", "type": "text", "length": "40" }, { "name": "Salary", "title": "Salary", "type": "float" }, { "name": "OrgUnit", "title": "Org Unit", "type":"text", "length":"128" }, { "name": "Gender", "title": "Gender", "type":"text", "length":"7", "valueMap": ["male", "female"] }, { "name": "MaritalStatus", "title": "Marital Status", "type": "text", "length": "10", "valueMap": ["married", "single"] } ], "dataFormat":"json", "operationBindings": [ { "operationType": "fetch", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" }, { "operationType": "add", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" }, { "operationType": "update", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" }, { "operationType": "remove", "dataProtocol": "postMessage", "dataURL": "/smartclient/data" } ] });
Finally, load this newly defined DataSource into the browser. For this edit the index.php and add the following code:
<%= javascript_include_tag "datasource/supplyitem" %> <%= javascript_include_tag "datasource/employee" %> <%= javascript_include_tag "smartclient_ui" %>
On the user interface, a change is required to allow users to switch the current DataSource. Add a form with a drop-down with the DataSources to switch and place it in front of the grids below the ListGrid. This requires putting the form in the HStack layout used for the buttons:
isc.HStack.create({ "membersMargin": 10, "ID": "gridButtons", "members": [ isc.DynamicForm.create({ "values": { dataSource: "Change DataSource" }, "items": [ { "name": "dataSource", show"title": false, "editortype": "select", "valueMap": ["supplyItem", "employees"], "change": function (form, item, value, oldValue) { if (!this.valueMap.contains(value)) return false; else { supplyItemGrid.setDataSource(value); advancedFilter.setDataSource(value); supplyItemGrid.filterData(advancedFilter.getCriteria()); } } } ] }), isc.IButton.create({ "top": 250, "title": "Edit New", "click": "supplyItemGrid.startEditingNew()" }), isc.IButton.create({ "top": 250, "left": 100, "title": "Save all", "click": "supplyItemGrid.saveAllEdits()" }), ] });
Finally we should change the controller, when the user selects the method, we can get the model by get_datasource method of the RPCManager helper class.
require 'RPCManager' class SmartclientController < ApplicationController def index end def data request = params[:smartclient] rpc = RPCManager.new(request) data_source = rpc.get_datasource case data_source when 'supplyitem' # select supplyitems table model = Supplyitem when 'employee' # select employees table model = Employee else # default model = Supplyitem end # set the table rpc.model = model # set the request parameters @result = rpc.processRequest render json: @result end end
This example now shows a data-driven DataSource that allows users to add/remove/update two DataSources with two different entity DataSource, and also apply various filter criteria built with the Filter Builder.
The complete code for this sample project can be downloaded from here.
6. smartclient gem
As we explained about this gem before, this gem has the RPCManager, DSRequest, DSResponse, DataSource helper classes.
You can modify this gem and compile again by your requirement.
Now we will show the code of the smartclient gem.
1) DSResponse
This class makes the JSON payload for the front-end as the response.
class DSResponse attr_accessor :data, :startRow, :endRow, :totalRow, :status @data = nil @startRow = 0 @endRow = 0 @totalRow = 0 @status = -1 def data=(value) @data = value end def startRow=(value) @startRow = value end def endRow=(value) @endtRow = value end def totalRow=(value) @totalRow = value end def status=(value) @status = value end end
2) DSRequest
require 'DataSource' =begin <summary> reference to the RPCManager executing this request will be stored in DSRequest. This has to be done because, while the request is being.executed, access will be required to various items such as the DataSource object, etc - These items will all be provided by the RPCManager class. </summary> =end class DSRequest attr_accessor :dataSource, :operationType, :startRow, :endRow, :textMatchStyle, :data, :sortBy, :oldValues, :advancedCriteria @dataSource = nil @operationType = nil @startRow = nil @endRow = nil @textMatchStyle = nil @componentId = nil @data = nil @sortBy = nil @oldValues = nil @advancedCriteria = nil @@obj = nil def initialize(data, model) @componentId = data[:componentId] @dataSource = data[:dataSource] @operationType = data[:operationType] @startRow = data[:startRow] @endRow = data[:endRow] @textMatchStyle = data[:textMatchStyle] @data = data[:data] @sortBy = data[:sortBy] @oldValues = data[:oldValues] @@obj = model end =begin <summary> The execute() method itself only loads the DataSource object then calls the DataSource's execute method for processing the request. </summary> <params> @datasource: DataSource object from the RPCManager helper class @@obj: model object that is mapped to the table </params> =end def execute ds = DataSource.new(@dataSource, @@obj) if ds.nil? return nil else return ds.execute(self) end end def advancedCriteria=(value) @advancedCriteria = value end end
3) DataSource
=begin <summary> This helper classes process the request after recieve from the DSRequest. The CRUD methods(add, remove, update, fetch) were supported. </summary> =end class DataSource attr_accessor :data_source @data_source = nil @model = nil @pk = nil def initialize(path, model) @model = model @pk = @model.primary_key() end =begin <summary> get the field content by the filed name </summary> =end def get_field(field_name) fields = @data_source['fields'] fields.each do | f | if f['name'] == filed_name return f end end return nil end =begin <summary> process the request </summary> =end def execute(request) operation_type = request.operationType case operation_type when 'fetch' @result = fetch(request) when 'add' @result = add(request) when 'remove' @result = remove(request) when 'update' @result = update(request) end return @result end private def buildStandardCriteria(request, table_name) query = 'SELECT * FROM ' + table_name + ' WHERE ' param = Array.new condition = '' request.data.each do |key, value| condition += "#{key} LIKE ? AND " param << "%" + value + "%" end q = condition[0, condition.rindex('AND ')] query += q order = '' unless request.sortBy.nil? request.sortBy.each do |idx| if idx.index('-') === nil order = " ORDER BY " + idx.to_s + " ASC" else order = " ORDER BY " + idx.to_s + " DESC" end end end query += order temp = Array.new temp << query temp.concat(param) return temp end def buildAdvancedCriteria(request, table) advancedCriteria = request.advancedCriteria criteria_query = buildCriterion(advancedCriteria) query = "SELECT * FROM " + table.to_s + " WHERE " + criteria_query[:query] # sort by order = '' unless request.sortBy.nil? request.sortBy.each do |idx| if idx.index('-') === nil order = " ORDER BY " + idx.to_s + " ASC" else order = " ORDER BY " + idx.to_s + " DESC" end end end query += order result = Array.new result << query result.concat(criteria_query[:values]) return result end def buildCriterion(advancedCriteria) criterias = advancedCriteria[:criteria] operator = advancedCriteria[:operator] values = Array.new result = '' criterias.each do | c | if c.has_key?(:fieldName) fn = c[:fieldName] end if c.has_key?(:operator) op = c[:operator] end if c.has_key?(:value) if c[:value] === true val = 1 elsif c[:value] === false val = 0 else val = c[:value] end end if c.has_key?(:start) start = c[:start] end if c.has_key?(:end) _end = c[:end] end if c.has_key?(:criteria) criteria = c[:criteria] else criteria = nil end if criteria == nil query = '' case op when 'equals' query = "#{fn} = ?"; values << val when 'notEqual' query = "#{fn} != ?"; values << val when 'iEquals' query = "UPPER(#{fn}) = ?" values << "UPPER('#{val}')" when 'iNotEqual' query = "UPPER(#{fn}) != ?" values << "UPPER('#{val}')" when 'greaterThan' query = "#{fn} > ?" values << val when 'lessThan' query = "#{fn} < ?" values << val when 'greaterOrEqual' query = "#{fn} >= ?" values << val when 'lessOrEqual' query = "#{fn} <= ?"; values << val when 'contains' query = "#{fn} LIKE ?"; values << "%#{val}%" when 'startsWith' query = "#{fn} LIKE ?"; values << "#{val}%" when 'endsWith' query = "#{fn} LIKE ?"; values << "%#{val}" when 'iContains' query = "#{fn} LIKE ?"; values << "%#{val}%" when 'iStartsWith' query = "UPPER(#{fn}) LIKE ?" values << "UPPER('#{val}%')" when 'iEndsWith' query = "UPPER(#{fn}) LIKE ?" values << "UPPER('%#{val}')" when 'notContains' query = "#{fn} NOT LIKE ?" values << "%#{val}%" when 'notStartsWith' query = "#{fn} NOT LIKE ?" values << "#{val}%" when 'notEndsWith' query = "#{fn} NOT LIKE ?" values << "%#{val}" when 'iNotContains' query = "UPPER(#{fn}) NOT LIKE ?" values << "UPPER('%#{val}%')" when 'iNotStartsWith' query = "UPPER(#{fn}) NOT LIKE ?" values << "UPPER('#{val}%')" when 'iNotEndsWith' query = "UPPER(#{fn}) NOT LIKE ?" values << "UPPER('%#{val}')" when 'isNull' query = "#{fn} IS NULL" when 'notNull' query = "#{fn} IS NOT NULL" when 'equalsField' query = "#{fn} LIKE ?" values << "CONCAT('#{val}', '%')" when 'iEqualsField' query = "UPPER(#{fn}) LIKE ?" values << "UPPER(CONCAT('#{val}', '%'))" when 'iNotEqualField' query = "UPPER(#{fn}) NOT LIKE ?" values << "UPPER(CONCAT('#{val}', '%'))" when 'notEqualField' query = "#{fn} NOT LIKE ?" values << "CONCAT('#{val}', '%')" when 'greaterThanField' query = "#{fn} > ?" values << "CONCAT('#{val}', '%')" when 'lessThanField' query = "#{fn} < ?" values << "CONCAT('#{val}', '%')" when 'greaterOrEqualField' query = "#{fn} >= ?" values << "CONCAT('#{val}', '%')" when 'lessOrEqualField' query = "#{fn} <= ?" values << "CONCAT('#{val}', '%')" when 'iBetweenInclusive' query = "#{fn} BETWEEM ? AND ?" values << start values << _end when 'betweenInclusive' query = "#{fn} BETWEEM ? AND ?" values << start values << _end end result = result.to_s + " " + query.to_s + " " + operator.to_s + " " else # build the list of subcriterias or criterions temp = result result1 = buildCriterion(c) result = temp.to_s + "(" + result1[:query] + ") " + operator + " " result1[:values].each do | value | values << value end end end q = result[0, result.rindex(operator)] criteria_result = Hash.new criteria_result[:query] = q criteria_result[:values] = values return criteria_result end =begin <summary> get the item list from the table </summary> <note>Before this method is called, the filter method should define in the model of the projects.</note> =end def fetch(request) table_name = @model.table_name data = request.data # check the advanced cretira unless request.advancedCriteria.nil? query = buildAdvancedCriteria(request, table_name) @obj_items = @model.find_by_sql(query) else unless request.data.empty? query = buildStandardCriteria(request, table_name) @obj_items = @model.find_by_sql(query) else @obj_items = @model.find(:all) end end objs_count = @obj_items.count # get the count of the obj_items endRow = (objs_count > 0)?objs_count - 1 : objs_count # make the Response result object response = DSResponse.new response.data = @obj_items response.startRow = 0 response.endRow = endRow response.status = 0 response.totalRow = objs_count return response end =begin <summary>Add new item</summary> =end def add(request) new_data = request.data @model.create(new_data) response = DSResponse.new response.data = new_data response.status = 0 return response end =begin <summary>Remove the selected item</summary> =end def remove(request) data = request.data id = data[@pk] # remove the item @model.destroy(id) response = DSResponse.new response.data = nil response.status = 0 return response end =begin <summary>Update the items</summary> =end def update(request) # get the old data from the request object old_data = request.oldValues # get the date from the request object update_data = request.data new_id = update_data[@pk] # merge to hash objects merged_data = old_data.merge!(update_data) merged_data.delete(@pk) #update @model.update(new_id, merged_data) response = DSResponse.new response.status = 0 return response end end
4) RPCManager
=begin <summary> Any action of the user with the DataSource will only call the RPCManager and will delegate all responsibility to it. The RPCManager will parse the payload and setup the DSRequest request and will call for the request's execute() method which will return the DSResponse object. The RPCManager will then convert this DSResponse into a suitable response and return it to the front-end. </summary> =end require 'DSRequest' require 'DSResponse' class RPCManager @request = nil @model = nil @temp_request = nil =begin <summary> Process the request with the model. </summary> <params> request: posted request parameters model: the object that is mapped to the table </params> =end def initialize(request=nil) # if is not wrapped in a transaction then we'll wrap it to make unified handling of the request if !check_transaction(request) req_hash = HashWithIndifferentAccess.new req_hash[:transaction] = HashWithIndifferentAccess.new req_hash[:transaction][:transactionNum] = -1 req_list = Array.new req_list << request req_hash[:transaction][:operations] = req_list @request = req_hash else @request = request end end def model=(model) @model = model end =begin <summary> Helper method to decide if request contains an advanced criteria or not </summary> <param name="req"></param> <returns></returns> =end def check_advanced_criteria(data) if data.include?(:_constructor) return true else return false end end =begin <summary> Returns true if the request has transaction support </summary> <returns></returns> =end def check_transaction(request) if request.include?(:transaction)# and request.include?(:operations) and request.include?(:transactionNum) return true else return false end end =begin <summary> Transforms a object object into a Json. Will setup the serializer with the appropriate converters, attributes,etc. </summary> <param name="dsresponse">the object object to be transformed to json</param> <returns>the created json object</returns> =end def processRequest response = processTransaction @result = { :response => response } return @result end =begin <summary> Select the model by the datasource </summary> <returns>Datasource name</returns> =end def get_datasource if @request.include?(:transaction) return @request[:transaction][:operations][0][:dataSource] else return @request[:dataSource] end end =begin <summary> Process the transaction request for which this RPCManager was created for </summary> <returns></returns> =end def processTransaction # retrieve the requests with data in form transaction_request = @request[:transaction] # store transaction num, we'll use it later to see if there was a transaction or not transaction_num = transaction_request[:transactionNum] # fetch the operations operations = transaction_request[:operations] # response list res_list = Array.new # transaction progress @model.transaction do begin operations.each do |op| # parase advanced criterias, if any advanced_criteria = parse_advanced_criterias(op) req = DSRequest.new(op, @model) unless advanced_criteria == nil req.advancedCriteria = advanced_criteria end # execute the request and get the response res = req.execute if res == nil res = DSResponse.new res.status = -1 end # store the response for later res_list << res end rescue ActiveRecord::RecordInvalid # if it occurs exception raise ActiveRecord::Rollback end end # if we have only one object, send directly the DSResponse if transaction_num == -1 response = DSResponse.new response.data = res_list[0].data response.startRow = res_list[0].startRow response.endRow = res_list[0].endRow response.totalRow = res_list[0].totalRow response.status = res_list[0].status return response end # iterate over the responses and create a instance of an anonymous class which mimics the required json responses = Array.new res_list.each do | response | res = DSResponse.new res.data = response.data res.startRow = response.startRow res.endRow = response.endRow res.totalRow = response.totalRow res.status = response.status responses << res end return responses end def parse_advanced_criterias(operations) data = operations[:data] if check_advanced_criteria(data) return data end return nil end end
You can find this gem on the rubygems sites. (https://rubygems.org/gems/smartclient) and you can review the source code from github or document of the rubygems site(http://rubydoc.info/gems/smartclient/0.0.7/frames).