Spring 2012 – Demo Project 3 The purpose of this project, called demo-proj3, is to set up an application that does similar things to what has to be done in project 3 but using PHP instead of Rails as the middleware. This will allow sharing of the PHP code with the students without giving away how to do this for the actual project 3. Database backend We will create a database called demo_proj3, that has a user named 'bob', and a password called 'somepass'. This database will have a table called users, to hold the users and authentication information. This database will also have a table called items, that keeps track of implementation items for an organization that is trying to improve its operation. A table called items_users will keep track of the users that are allowed to edit an item. Database creation and addition of pgcrypto We can start by creating the database: psql -U cent285man -W cent285db Password for user cent285man: psql (8.4.9) Type "help" for help. cent285db=# create database demo_proj3 owner bob; CREATE DATABASE cent285db=# \c demo_proj3 Password for user cent285man: psql (8.4.9) You are now connected to database "demo_proj3". demo_proj3=# \i /usr/share/postgresql/8.4/contrib/pgcrypto.sql That will be accompanied by a SET, and a lot of CREATE FUNCTION commands. Recall that the pgcrypto functions are added so that we can use SHA-256 encryption. Here is a start into a SQL script that creates the tables: -- createTables.sql -- create sequences drop sequence if exists user_id_seq cascade; create sequence user_id_seq; drop sequence if exists item_id_seq cascade; create sequence item_id_seq; -- create tables drop table if exists users cascade; create table users( id integer not null default nextval('user_id_seq'), name text, username text unique, 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Spring 2012 – Demo Project 3
The purpose of this project, called demo-proj3, is to set up an application that does similar things to what has to be done in project 3 but using PHP instead of Rails as the middleware. This will allow sharing of the PHP code with the students without giving away how to do this for the actual project 3.
Database backend
We will create a database called demo_proj3, that has a user named 'bob', and a password called 'somepass'. This database will have a table called users, to hold the users and authentication information. This database will also have a table called items, that keeps track of implementation items for an organization that is trying to improve its operation. A table called items_users will keep track of the users that are allowed to edit an item.
Database creation and addition of pgcrypto
We can start by creating the database:
psql -U cent285man -W cent285dbPassword for user cent285man: psql (8.4.9)Type "help" for help.
cent285db=# create database demo_proj3 owner bob;CREATE DATABASEcent285db=# \c demo_proj3 Password for user cent285man: psql (8.4.9)You are now connected to database "demo_proj3".demo_proj3=# \i /usr/share/postgresql/8.4/contrib/pgcrypto.sql
That will be accompanied by a SET, and a lot of CREATE FUNCTION commands. Recall that the pgcrypto functions are added so that we can use SHA-256 encryption.
Here is a start into a SQL script that creates the tables:
-- createTables.sql
-- create sequencesdrop sequence if exists user_id_seq cascade;create sequence user_id_seq;drop sequence if exists item_id_seq cascade;create sequence item_id_seq;
-- create tablesdrop table if exists users cascade;create table users( id integer not null default nextval('user_id_seq'), name text, username text unique,
/*insert into users (name,username) values ('Jane Doe','janedoe');insert into users (name,username) values ('John Doe','johndoe');insert into items (description, created_at, updated_at) values ('item1',now(),now());insert into items (description, created_at, updated_at) values ('item2',now(),now());insert into items_users (user_id, item_id) values (1,1);insert into items_users (user_id, item_id) values (1,2);insert into items_users (user_id, item_id) values (2,1);select * from items_users;delete from items where id=2;select * from items_users;delete from users where id=2;select * from users;*/
Lines 4-7 create the sequences needed for the users and items tables. Lines 11-21 create the users table. Lines 23-33 create the items table. Each item will eventually be ranked. The num_low, num_med and num_high fields will hold the number of low votes, the number of medium votes and the number of high votes, respectively.
LInes 35-40 define the items_users table. This is the table that will keep track of the user(s) that can edit an item. Note, on line 37, that only the item_id is defined to cascade on delete. This means that when an item is deleted, any entries in the items_users table that refers to that deleted item, will also be deleted. Conversely, no such relation is made for the
2
15
1617
1819
2021
2223
2425
2627
2829
3031
3233
3435
3637
3839
40
4142
434445
4647
4849
5051
5253
5455
5657
5859
user_id field. This will prevent a user from being deleted, if that user has entries in the items_users table. Also note on line 39 that we require the combination of the user_id and item_id to be unique. This will prevent duplicate records in this table.
The test data shown from line 42 on is designed to test the tables. After the tables are shown to work okay, this test data will be deleted. As expected, deleting item with id=2 causes the items_users table to have rows with item_id =2 to be deleted. Also, the attempt to delete user id = 2 fails, as this user is still referenced by the items_users table.
Adding a trigger for the updated_at field
When, we update an item, we want the updated_at field to reflect the time of update. This can be done with a trigger. Here is a second SQL script called, “createTrigger.sql” that does this:
-- createTrigger.sql drop language plpgsql cascade; create language plpgsql; create or replace function update_timestamp() returns trigger as $func$ begin NEW.updated_at := now(); return NEW; end; $func$ language 'plpgsql';
create trigger update_items_updatedat before update on items for each row execute procedure update_timestamp(); -- test data insert into items (description, created_at, updated_at) values ('item1',now(),now()); insert into items (description, created_at, updated_at) values ('item2',now(),now()); select description,created_at,updated_at from items; update items set description='item2 updated' where id=2; select description,created_at,updated_at from items;
Lines 4-12 define the function update_timestamp(). This function uses the now() function to obtain the new value for the updated_at field. In terms of how the plpgsql language works, NEW refers to the new record set. Returning this record set to the calling program causes the record to be updated.
On lines 14-16, a trigger is created. On lines 14-15, we create a trigger that is called anytime the items table is updated. On line 16, the update_timestamp() function defined earlier is called to update the updated_at field.
3
1
23
45
67
89
1011
12
131415
1617
1819
2021
2223
2425
Lines 18 on just test this trigger. Reloading this script will show the updated_at field from the items table being updated properly. After verifying that this works, those lines can be commented out.
Creating functions for inserting users
Now we can add a script that adds functions that helps us to add a user, and authenticates a user. Here is the script, “userFuncs.sql”:
-- userFuncs.sql
create or replace function doEncrypt(_text text) returns text as $func$ begin return encode(digest(_text,'sha256'),'hex'); end; $func$ language 'plpgsql'; create or replace function makeSalt(_text text) returns text as $func$ begin return doEncrypt(_text||now()); end; $func$ language 'plpgsql'; create or replace function makePass(_text text) returns text as $func$ begin return doEncrypt(_text||makeSalt(_text)); end; $func$ language 'plpgsql'; create or replace function addUser(_name text,_user text, _pass text, _div text, _dept text) returns boolean as $func$ begin insert into users (name, username, enc_pass, salt, division, department) values (_name, _user, makePass(_pass),makeSalt(_pass), _div, _dept); return 't'; end; $func$ language 'plpgsql'; create or replace function getID(_user text, _pass text) returns integer as $func$
4
1
234
56
78
910
1112
1314
1516
1718
1920
2122
2324
2526
2728
2930
3132
3334
3536
3738
3940
4142
4344
4546
declare rec record; begin select into rec * from users where username=_user; if found then if doEncrypt(_pass||rec.salt) = rec.enc_pass then return rec.id; else return -1; end if; else return -1; end if; end; $func$ language 'plpgsql'; select doEncrypt('mypass'); select makeSalt('mypass'); select makePass('mypass'); select addUser('Vern Takebayashi','vern','mypass','div1','deptA'); select addUser('Jane Doe','janedoe','janey','div1','deptB'); select getID('vern','mypass'); select getID('vern','mYpass'); select getID('vurn','mypass'); select getID('janedoe','janey');
Lines 3-10 define the doEncrypt() function. This function uses the digest() function, defined in pgcrypto. That digest() function encrypts the passed text using SHA-256. Then, the encode() function creates hexadecimal hash from the encrypted text.
Lines 12-19 are used to create the salt. This is done by concatenating the passed text with the string returned by the now() function and encrypting the resulting string.
Lines 21-28 define the makePass() function. This function just encrypts the string that results from concatenating the passed string with the result of calling makeSalt() on the passed string. This general scheme for creating the encrypted password makes it difficult to brute-force the password using rainbow tables. The rainbow tables cannot take into account a salt that depends on the time the original password was entered.
LInes 30-42 define and addUser() function that inserts the user with an encrypted password. The salt is stored at the same time. Note that the password field is not even inserted as part of the record. Only the encrypted password is inserted, and the value of the encrypted password is useless without also knowing the salt.
Lines 44-62 define the getID() function. This function will check to see if the username exists. If the username is not found, then a -1 is returned on lines 57-58. If the username is found, the function obtains the string that results from encrypting the concatenated string formed by combining the passed string with the stored salt. The resulting encrypted string is compared to the stored password. If this matches, the function returns the id value for the
5
47
4849
5051
5253
5455
5657
5859
6061
6263
6465
6667
6869
7071
72
record. If this does not match, the function returns a -1.
The rest of the lines from 64 on just insert some users and verifies that the getID() function returns the user's id if the username and password match. Otherwise, a -1 is returned.
Adding initial data
Now that we can add users, let's create a script called “initUsers.sql” to add the initial users. This will allow us to make the initial user (id = 1), the admin user. Here is that script:
create or replace view users_view as select id,name,username,division,department from users;
On lines 10-12 a view that will select only the fields that would be needed by the application. There is no need to get the enc_pass or salt fields. This will allow gettting the pertinent information with a simple query:select * from users_view;
At this point, it makes sense to have a SQL script that calls the other SQL scripts. This will make it easy to call all the right scripts in the right order. Here is the master SQL script, “makeAll.sql”:
After reloading “makeAll.sql”, the database is ready to be accessed via PHP.
Using PHP to show the users
The first step is to create a PHP file that allows connecting to the database. Normally this file would be located outside the Apache document root, but to simplify things for demo purposes we will just place this file in the same location as the other project files. Here is the PHP file to include to allow connections, “demo_proj3.php”:
Create a project called “DemoProj3” with a package name of “test”. After removing all the servlet files, modify “DemoProj3.gwt.xml”:
src/test/DemoProj3.gwt.xml
<?xml version="1.0" encoding="UTF-8"?>
7
3
45
67
89
12
34
56
78
910
1112
1314
1
<module rename-to='demoproj3'> <inherits name='com.google.gwt.user.User'/> <inherits name='com.google.gwt.http.HTTP'/> <inherits name='com.google.gwt.user.theme.clean.Clean'/> <collapse-all-properties /> <!-- Specify the app entry point class. --> <entry-point class='test.client.DemoProj3'/></module>
Line 3 is the standard GWT module that is used for the standard interface widgets. Line 4 is needed to make HTTP requests. Line 6 is used so that support for the different browsers is combined. This is mainly to save compilation time.
Now let's modify the main HTML file, “DemoProj3.html”.
war/DemoProj3.html
<!doctype html><html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <link type="text/css" rel="stylesheet" href="DemoProj3.css"> <title>Demo Project 3</title> <script type="text/javascript" language="javascript" src="demoproj3/demoproj3.nocache.js"></script> </head> <body> <!-- JavaScript must be enabled --> <noscript> <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif"> Your web browser must have JavaScript enabled in order for this application to display correctly. </div> </noscript>
<h1>Demo Proj3</h1> </body></html>
The main things that are changed from the default values are the removal of the code needed for browser history support and the table. Line 6 and line 22 have been changed to reflect the name of this project.
Now, let's edit the main java file, “DemoProj3.java”
public class DemoProj3 implements EntryPoint { VerticalPanel mainPanel = new VerticalPanel(); String baseURL = "http://localhost/demo-proj3/"; public void onModuleLoad() { String url = baseURL + "showUsers.php"; getRequest(url, "getUsers"); } private void getRequest(String url, String requestName) { final RequestBuilder rb = new RequestBuilder(RequestBuilder.GET,url); try { rb.sendRequest(null, new RequestCallback() { public void onError(final Request request, final Throwable exception) { Window.alert(exception.getMessage()); } public void onResponseReceived(final Request request, final Response response) { Window.alert(response.getText()); } }); } catch (final Exception e) { Window.alert(e.getMessage()); } } // end getRequest}
Right-click on DemoProj3 in the Package Explorer, select Google, and the GWT Compile. Click on the Advanced drop down and add to the “Additional compiler arguments” the switch:-draftCompile
Then, click Apply, and then Compile.
Copying files to apache server directory
The GWT compiler converts the Java source into JavaScript files. These files need to be copied to the directory where they can be served out by the Apache server.
Each copy command should be on one line. Next, you can run the program by starting a browser and going to “http://localhost/demo-proj3/DemoProj3.html”:
When this is run, an alert showing the user data in JSON format should be displayed.
Now, we need to add a new class file to allow reading of the JSON data directly into a Java ArrayList. We also need to modify the main Java program so that it can store the data in a CellTable.
Adding the User class
We need to define a Java class that can be used to get the JSON data into our GWT program. This class will extends JavaScriptObject, so that we can use native JavaScript methods to read from the JSON data into this Java class.
Right-click on /src/test/client and select New, then Class. Set the name to “User”. Make the contents of this file as follows:
package test.client;
import com.google.gwt.core.client.JavaScriptObject;class User extends JavaScriptObject{ protected User() {} public final native int getID() /*-{ return this.id; }-*/; public final native String getName() /*-{
10
1
234
56
78
910
1112
13
return this.name; }-*/; public final native String getUsername() /*-{ return this.username; }-*/; public final native String getDivision() /*-{ return this.division; }-*/; public final native String getDepartment() /*-{ return this.department; }-*/;}
Whenever you are creating classes for this purpose, the construct must have a visibility modifier of protected, and the constructor's body must be empty. This is shown on lines 6-7. Lines 8-11 define the getID() method. The body of this method must start with:/*-{
and end with:}-*/;
The body just takes the current JavaScriptObject (this) and returns the value for the appropriate key. In this case, on line 10, we are returning the value of the id key.
The same type of definition is used for all the other methods.
Now, let's modify the main Java file, “DemoProj3.java”:
The new lines, lines 10-15, lines 21-23, lines 25-43, line 47, line 64-66, lines 74-109, and lines 110-113, are shown shaded.
Lines 10-15 add the import statements needed for the Java classes that we are adding for this version of the program. Line 10 adds the RootPanel class. This class is needed to insert anything into the main window of the application. The RootPanel class will be added for just about every program. Line 11 adds the JsonUtils class. We will use the safeEval() static method from this class on line 112, to safely evaluate the JSON formatted string. Line 12 adds the ArrayList class. The ArrayList class will be what we use to hold the data within the GWT application. Line 14 adds the CellTable class. We will use a CellTable to display the users data. Line 15 adds the TextColumn class that will be used to populate the CellTable.
Lines 21-22 construct an ArrayList<MyUser> to hold the users data within the GWT application. Although we will initially read our data into a JsArray, the ArrayList is the more flexible storage format. Line 23 defines the JsArray<User> called jsonData.
At this point, it is a good idea to try to understand how these two data types will be used. When the server returns the data, the data is formatted as JSON. To get the JSON data into the program, we need to read it in as an array of JavaScript objects. That is where the User
13
84
8586
8788
8990
9192
9394
9596
9798
99100
101102
103104
105106
107108
109110
111112
113114
class comes into play. The User class uses JSNI (JavaScript Native Interface) methods, to extract the data from the JSON object. This is why the data is read in initially as a JsArray<User>. Although a JsArray is an array, it is not as useful to our GWT program as an ArrayList would be. Therefore, we need to pull the data from the JsArray<User> and put it into an ArrayList<MyUser>. The MyUser class is a private class defined inside the DemoProj3 class. Defining MyUser as an inner class makes it so that you don't have to provide accessor methods for the instance variables. Once we have the data in the ArrayList<MyUser>, it will be easier to manipulate the data, such as putting the data into a CellTable.
The aforementioned MyUser class is defined on lines 25-42. As stated earlier, this is defined as an inner class so that accessor methods are not needed to get to the instance variables.
Line 47 adds the VerticalPanel called “mainPanel” to the RootPanel.
Lines 64-66 call the showUsers() method. This technique is used so that the getRequest() method can be reused for returning other data from the database server. Additional selection statements can be added in the onResponseReceived() method to handle other requests. In this way, the response data can be passed off to the appropriate method for handling.
Lines 75-109 define the showUsers() method. Line 76 calls the getData() method that gets the JSON data and returns it as a JsArray<User>. The getData() method is defined on lines 110-113. Lines 78-83 define a for loop that populates the ArrayList<MyUser> called “users”. This is the ArrayList<MyUser> that was defined and initialized on lines 21-22. Lines 84 initializes the CellTable. Lines 85-93 construct a TextColumn that will be used for the name column of the CellTable. All this code does is override the getValue() method for the TextColumn class. Note on line 91 how the variable name can be accessed directly because MyUser is defined as an inner class. Similarly the TextColumn for the username is constructed on lines 94-102. Lines 104 and 105 add the TextColumns that were constructed earlier. Finally, on line 107, the data from the ArrayList<MyUser> is added into the table. Line 108 adds the table into the mainPanel.
Lines 110-113 define the getData() method. In some examples of using JSON with GWT, you will see this type of method use the native JavaScript eval() method. This is considered to be okay by many people since the data is coming from the database server, and is supposedly a trusted source. But, since attacks involving JSON often use JsArrays, it doesn't hurt to be a little safer and use JsonUtils.safeEval() instead. This will prevent attacks that put JavaScript commands inside the JSON data.
At this point, the CellTable only shows two columns. This was done to keep the code a
14
little shorter. The amount of code to get the JSON data from the server into a GWT CellTable may seem lengthy, but most of the code is simple once you get used to this method. It is possible to use GWT's JSON parser and extract the data individually, but this method is simpler.
Here is the code updated to show all of the columns:
The new lines, lines 85-93, lines 112-129, line 130, lines 133-134, are shown shaded. This just adds in three more columns for the CellTable.
Ability to login to application
Let's add the ability to login to the application. In the GWT application, let's start by adding a panel that contains a TextBox widget and a PasswordTextBox widget to get the username and password respectively. Then, we will add the ability to send a post request to a PHP script.
} public void onModuleLoad() { String url = baseURL + "showUsers.php"; //getRequest(url, "getUsers"); RootPanel.get().add(mainPanel); init(); } private void getRequest(String url, final String requestName) { final RequestBuilder rb = new RequestBuilder(RequestBuilder.GET,url); try { rb.sendRequest(null, new RequestCallback() { public void onError(final Request request, final Throwable exception) { Window.alert(exception.getMessage()); } public void onResponseReceived(final Request request, final Response response) { if (requestName.equals("getUsers")) { showUsers(response.getText()); } } }); } catch (final Exception e) { Window.alert(e.getMessage()); } } // end getRequest() private void postRequest(String url, String data, final String requestName) { final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,url); rb.setHeader("Content-type", "application/x-www-form-urlencoded"); try { rb.sendRequest(data, new RequestCallback() { public void onError(final Request request, final Throwable exception) { Window.alert(exception.getMessage()); } public void onResponseReceived(final Request request, final Response response) { if (requestName.equals("login")) { String resp = response.getText().trim(); int id = Integer.parseInt(resp); if (id < 1) { usernameBox.setText(""); passBox.setText(""); }
19
66
6768
6970
7172
7374
7576
7778
7980
8182
8384
8586
8788
8990
9192
9394
9596
9798
99100
101102
103104
105106
107108
109110
111112
113114
115116
117118
119120
121122
123
else { mainPanel.clear(); Label status = new Label("id: " + id + " logged in"); mainPanel.add(status); } } } }); } catch (final Exception e) { Window.alert(e.getMessage()); } } // postRequest() ends private void showUsers(String responseText) { jsonData = getData(responseText); User user = null; for (int i = 1; i < jsonData.length(); i++) { user = jsonData.get(i); users.add(new MyUser(user.getID(), user.getName(),user.getUsername(),user.getDivision(), user.getDepartment())); } CellTable<MyUser> table = new CellTable<MyUser>(); TextColumn<MyUser> idColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) { return "" + user.id; } }; TextColumn<MyUser> nameColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) { return user.name; } }; TextColumn<MyUser> usernameColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) { return user.username; } }; TextColumn<MyUser> divColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) {
20
124
125126
127128
129130
131132
133134
135136
137138
139140
141142
143144
145146
147148
149150
151152
153154
155156
157158
159160
161162
163164
165166
167168
169170
171172
173174
175176
177178
179180
181
return user.division; } }; TextColumn<MyUser> deptColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) { return user.department; } }; table.addColumn(idColumn,"id"); table.addColumn(nameColumn,"Name"); table.addColumn(usernameColumn,"Username"); table.addColumn(divColumn,"Division"); table.addColumn(deptColumn,"Department"); table.setRowCount(users.size(),true); table.setRowData(0,users); mainPanel.add(table); } // end showUsers() private JsArray<User> getData(String json) { return JsonUtils.safeEval(json); } private void init() { Label usernameLabel = new Label("Username: "); usernameBox = new TextBox(); HorizontalPanel userRow = new HorizontalPanel(); userRow.add(usernameLabel); userRow.add(usernameBox); loginPanel.add(userRow); Label passLabel = new Label("Password"); passBox = new PasswordTextBox(); HorizontalPanel passRow = new HorizontalPanel(); passRow.add(passLabel); passRow.add(passBox); loginPanel.add(passRow); loginButton.addClickHandler(this); loginPanel.add(loginButton); mainPanel.add(loginPanel); }}
The new lines, lines 10-17, line 25, lines 32-35, lines 55-66, line 70, lines 99-137 and lines 207-224, are shown shaded.
Lines 10-17 are the import statements for the new classes being used.
Line 10 imports the ClickEvent class. This is needed to be able to respond to the mouse click events. In our case this means responding to the mouse clicking on the “Login” button. Line 11 imports the ClickHandler interface. Note on line 25 that we implement this interface. This also is needed to respond to the click events.
21
182
183184
185186
187188
189190
191192
193194
195196
197198
199200
201202
203204
205206
207208
209210
211212
213214
215216
217218
219220
221222
223224
225
Line 12 imports the HorizontalPanel class. This is used when we are setting up the input boxes for the username and password. In the init() method, on lines 207-224, the HorizontalPanel is used to place the label and the textbox on the same line. This is done on lines 211-213 and again on lines 217-219.
Line 13 imports the Label class. This is used in the init() method on lines 209 and 215. These Labels are just used to label the input boxes for username and password.
Line 14 imports the Button class. A Button is used as the button the user presses when logging in. This is added in the init() method on lines 221-222.
Line 15 imports the TextBox class. This is used in the init() method on lines 210 and 213. Also, that TextBox object is initialized on line 33. The value of this TextBox is used on line 60 to get the username that is entered.
Line 16 imports the PasswordTextBox class. This is a special TextBox that puts special characters to the screen so that the password is not visible to someone looking over the user's shoulder when they login. The PasswordTextBox object is initialized on line 34, and is used on lines 216 and 219 in the init() method. The value of this PasswordTextBox is used on line 62 to get the value of the password that is entered.
Line 17 imports the URL class. The URL.encode method is used on lines 59-62 to encode the string values so they can be passed by HTTP post methods.
On line 25, we add the ClickHandler interface so that our application can respond to ClickEvents. The onClick() method defined on lines 55-66 is used to respond to ClickEvents.
Line 32 adds an additional VerticalPanel object that will be used for the login panel. Line 33 and line 34 are used to declare the TextBox and PasswordTextBox objects that will be used for the username and password respectively. The reason why they are declared here, is so that they will be visible inside the init() method, the onClick() method, and the postRequest() method (on lines 121 and 122).
Lines 55-66 define the onClick() method that is required whenever the ClickHandler interface is implemented. On line 57, we get the object that initiated the ClickEvent. If that object is the loginButton, the if statement defined on lines 58-64 is executed. Lines 59-62 set up the encoded string that will pass the username and password via POST. The names of the keys and the values are encoded so that they can be sent via HTTP POST. Line 63 forms the URL by adding the baseURL to “login.php”. This is the PHP script that will handle the login. Finally, on line 64, the postRequest() method is called.
On line 70, the getRequest() call is commented out so that it does not get called at that point.
22
Lines 99-137 define the postRequest() method. The first parameter for this method is a String that contains the URL to post to. The second parameter for this method is the encoded String data that is to be posted. The third parameter is a String that identifies the specific POST request that is being made. This last parameter is what is used inside the onResponseReceived() method, defined on lines 114-131. If you make additional post requests, you can respond to those using the same postRequest() method by changing the three parameters.
In this case, the important steps are in the if statement defined on lines 117-130. This will not make sense without understanding what is returned by the “login.php” script. The “login.php” script will return an integer. If that integer is greater than 0, it means that the user has been authenticated and the number is the user's id. If the number is less than 1, the “login.php” script has returned a -1, meaning a failed authentication. The details of how this is accomplished will be gone over when the “login.php” script is explained later. So, if “login.php” returns a -1, the if statement on lines 120-123 is executed. That if statement will just clear the input boxes. On the other hand, if a number 1 or larger is returned, the mainPanel is cleared on line 125. Then a Label is created and displayed showing the id number and a message showing the user logged in.
Lines 207-224 define the init() method that sets up the input boxes and button for the user to attempt login. Most of what goes on here has already been explained. The one line to take note of is line 221. Without the addClickHandler() method, the application cannot respond to the ClickEvent that is generated in response to clicking on loginButton.
login.php
Now, let's take a look at the PHP script, “login.php”, that is posted to in the authentication process.
Lines 2-3 connects us to the database. Line 7 sets the value of $out to -1. Lines 8-17 define an if statement that is executed as long as values have been posted in the $_POST array. Lines 9 and 10 get the values that have been posted for username and password, respectively. Line 11 defines a SQL query that calls the getID() plpgsql function. On lines 12, 13 and 14, we prepare the SQL statement, and then execute it. By using the prepare/execute technique, SQL injection attacks are blocked. Even if someone tried to use a typical SQL injection, the getID() function would just return a -1 anyway. On line 15, we fetch the result of running that SQL statement. On line 16, we obtain the value of the “getid” key. This value will either be a number 1 or greater if authenticated, or a -1 if not authenticated. Finally, on line 19 that value is echoed to the screen for the GWT application to pick up as the response that is received.
Now that the basic login mechanism works, we can make the GWT application do something different depending on which user logs in. If the admin user logs in, a user id of 1 is returned. In that case, let's make the GWT application show the users view. If someone else logs in successfully, the GWT application will just show the id for now.
Showing the users view if admin logs in
Here is the modified part of the code:
// lines above here unchanged } // end getRequest() private void postRequest(String url, String data, final String requestName) { final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,url); rb.setHeader("Content-type", "application/x-www-form-urlencoded"); try { rb.sendRequest(data, new RequestCallback() { public void onError(final Request request, final Throwable exception) { Window.alert(exception.getMessage()); } public void onResponseReceived(final Request request, final Response response) { if (requestName.equals("login")) { String resp = response.getText().trim(); int id = Integer.parseInt(resp); if (id < 1) { usernameBox.setText(""); passBox.setText(""); }
24
19
else if (id == 1) { mainPanel.clear(); String url = baseURL + "showUsers.php"; getRequest(url,"getUsers"); } else { mainPanel.clear(); Label status = new Label("id: " + id + " logged in"); mainPanel.add(status); } } // ends if (requestName.equals("login")) } // onResponseReceived() ends }); // new RequestCallback ends } catch (final Exception e) { Window.alert(e.getMessage()); } } // postRequest() ends// lines below here unmodified
The new lines are shown shaded. All they do is if the id returned is a 1, then the getRequest() that brings up the users view is called.
The next thing to do is to create a view for the items that are editable for the logged in user. Before we do this on the GWT side, we can return to the database and create a function for adding an item for a user, that checks to see if the user id is valid, then inserts the item and updates the items_users table.
Adding items
Adding an item is more involved than adding a user. Every item is associated with a user. So, we have to check if the user id is valid before inserting an item. In addition, the item can be set as editable by other departments. An item will be editable by the division chair of the original item submittor. But, the item is not editable by other division chairs, only be other departments.
The steps involved in submitting an item would be:
1. Check if the user id for the submittor is valid.
2. If the user id is valid, insert the item into the items table and save the newly generated item id.
3. Update the items_users table by inserting a record for the submittor's id and the item id. Add in a record for the division chair of the submittor's department.
4. Finally, add in records for the departments that have been designated as additional editors for that item.
This means we need functions at the database level that can handle this type of insertion.
25
We will also need to build up an entry form in GWT that can collect this kind of information.
SQL script containing functions for inserting an item
Let's add another SQL script that adds functions that make it easier to insert an item.
-- itemFuncs.sql
create or replace function addItemUser(_uid integer, _id integer) returns boolean as $func$ begin insert into items_users (user_id, item_id) values (_uid, _id); return 't'; end; $func$ language 'plpgsql';
create or replace function addOtherEditors(_others text, _id integer) returns boolean as $func$ declare v_id integer; my_array text[]; begin my_array := string_to_array(_others,','); for i in 1..array_length(my_array,1) loop v_id := cast (my_array[i] as integer); perform addItemUser(v_id,_id); end loop; return 't'; end; $func$ language 'plpgsql'; create or replace function addItem(_uid integer, _desc text, _just text, _others text) returns boolean as $func$ declare rec record; rec2 record; div text; dept text; chair_id integer; it_id integer; begin select into rec * from users where id = _uid; if not found then return 'f'; else insert into items (description,justification, origin_department,created_at,updated_at) values (_desc, _just, rec.department,now(), now()); it_id := currval('item_id_seq');
26
1
234
56
78
910
1112
1314
1516
1718
1920
2122
2324
2526
2728
2930
3132
3334
3536
3738
3940
4142
4344
4546
4748
4950
51
div := rec.division; select into rec2 * from users where department=div; chair_id := rec2.id; perform addItemUser(_uid, it_id); if rec.id <> chair_id then perform addItemUser(chair_id, it_id); end if; if _others <> '' then perform addOtherEditors(_others,it_id); end if; return 't'; end if; end; $func$ language 'plpgsql';/*select addItem(2,'item1','just1','4,5');select addItem(3,'item2','just2','4');select addItem(3,'item3','just3','');select addItem(10,'fakeitem','nojust','');*/
On lines 3-12 the addItemUser() function is defined. This function will be used to insert records into the items_users table. There is nothing fancy about this function in terms of SQL. However, this function does not return any useful result when it is run. This can be a problem when it is called inside another plpgsql function.
When calling a plpgsql function inside another plpgsql function, be careful if the called function does not return anything that is used by the calling program. If you do this, you need to use call the function using perform. That is, you call the function this way:perform func(arg1, arg2,...);
where func() is the plpgsql function that does not return anything used by the calling program. This will be shown in the next function.
Lines 14-30 define the addOtherEditors() function. This function will be used to add the other users that can edit an item. The first argument to be passed to this function is a comma-delimited string that contains a list of all the additional user ids. So, if you wanted the users with the ids 3, 4 and 6 to be added, you would pass a string like '3,4,6'. The second argument that is passed to addOtherEditors() is the id of the item that has just been added. So, if you wanted to call addOtherEditors() to add the users with ids 3,5,6 for an item with an id of 4, you would make this call:addOtherEditors('3,5,6',4);
Lines 18 and 19 declare local variables that are used in this function. Lines 21-28 form the body of this function. On line 22, we use the string_to_array() function to split the
27
52
5354
5556
5758
5960
6162
6364
6566
6768
6970
7172
string with the id numbers apart, and obtain an array of strings. In this call:string_to_array(_others,',')
the first argument, _others, is the comma-delimited string of id numbers. The second argument, the comma, is the delimiter used to perform the split.
Line 23-26 define a for loop that iterates over the user id numbers. Note that arrays in plpgsql start with an index of 1 (not zero). On line 24, we CAST the string into an integer that we store in the v_id variable. We need to perform this conversion as the string_to_array () function returns an array of strings. On line 25, we call the addItemUser() function by using the perform clause. If you just called addItemUser() without preceding the function name with perform, you would get an error that looks like this:
psql:itemFuncs.sql:30: ERROR: syntax error at or near "addItemUser"LINE 1: addItemUser( $1 , $2 ) ^QUERY: addItemUser( $1 , $2 )CONTEXT: SQL statement in PL/PgSQL function "addothereditors" near line 8
This error message does not identify the error properly, and points towards the wrong line. The error is not anywhere near line 8, and it is not really a syntax error. So, if you see something like this mysterious error, think about if you are calling a plpgsql function from within another plpgsql function. If the called plpgsql function does not return anything that is used by the calling function, then you need to make the call using perform.
Another important thing to note is the choice of local variable names. Make sure that you don't choose any local variable names that match field names from the database table(s) that you are querying. This will result in difficult to track down errors. So, we use names like v_id instead of id, since id is the name of a field in the users and items tables. If you have a plpgsql function that has an unexplained syntax error or simply produces the wrong result, double-check to make sure your local variable names (defined in the declare section), don't match any field names from your database.
Paying attention to the last two paragraphs can help avoid wasting a lot of time trying to get a plpgsql function to work. Using perform to call any plpgsql function that does not return a result that is used, and avoiding local variable names that match database field names are very important techniques.
Lines 32-65 define the addItem() function. This function does a lot of things, but it is easier to perform those tasks at the database level, than in a PHP or Rails script. Lines 36-41 just declares the local variables needed by this function. On line 43, we attempt to return a
28
row from the users table where the id matches the passed id. On lines 44-45, we end the function and return a False if no users have that id. If the user id is valid, the else statement defined on lines 46-61 is executed.
On lines 47-49, we perform the insertion of the item into the items table. Note on lines 48-49 we insert the department of the submitter into the origin_department field. On line 50, we store the newly generated item id in the variable, it_id. On lines 51-52, we get the division for the submitter, and get the user whose department matches this division. Only division chairs have a department that matches the division name. On line 53, we store the id for the division chair.
On line 54, we use the perform clause to call the addItemUser() function to insert a record into the items_users table. This is the insertion for the submitting user and the newly inserted item.
On lines 55-57, an if statement is used to insert a record into the items_users table for the division chair. But, if the submitter is the division chair, this insertion is not performed.
On lines 58-60, the addOtherEditors() function is called, using perform. Note that this call is only made if the string containing the id numbers for additional editors is not an empty string.
If we wanted to test these functions we could use SQL statements like:select addItem(3,'item1','just1','4,5');
This would add an item for a user with an id of 3. The item description would be 'item1', and the item's justification would be 'just1'. The additional users allowed to edit this item would be the users with id numbers of 4 and 5. If the user with an id of 3 is not the division chair, the division chair for that user's department would also be added as an editor.
Generating initial data and creating an item_view
To make it easier to develop the GWT code that displays the items, we should add some items for the users. This can be done with a SQL script, called “itemData.sql”:
We should create another script, called “itemView.sql”, that creates a database view that can be used to display a user's items. Here is the code for that script:
29
1
23
45
67
8
-- itemView.sqlcreate or replace view item_view as select users.id as uid, users.name, users.username, users.division, users.department, items.id as iid, items.description, items.justification, items.origin_department, items.created_at, items.updated_at from users inner join items_users on users.id=items_users.user_id inner join items on items_users.item_id=items.id; -- test queriesselect name,department,origin_department,description from item_view where uid=2;
The key thing about generating a view that involves joining several tables is to make sure that if you have repeated field names, as we do in this case with the id fields, to use aliases for those fields. So on line 3, we create an alias for users.id called “uid”. Similarly on line 4, we create an alias for items.id called “iid”. Lines 10 on just show a sample query using this view. Here is the result of loading the “itemView.sql” script:
\i itemView.sqlCREATE VIEW name | department | origin_department | description ----------+------------+-------------------+------------- Jane Doe | div1 | div1 | item1 Jane Doe | div1 | deptA | item2 Jane Doe | div1 | deptA | item3 Jane Doe | div1 | deptB | item4(4 rows)
Jane Doe is a division chair, so she can see items submitted by her (item1), as well as items submitted by departments in her division (item2, item3 and item4).
Creating a PHP script that can send the items
Now that we created a view in the database for viewing items, let's create a PHP script that will return the items in JSON format. Here is the script “showUserItems.php”:
<?php require_once('demo_proj3.php'); $pdo = connect(); if (!$pdo) { die("Could not connect"); } //$_POST['uid'] = 6; if (count($_POST) > 0) { $uid = $_POST["uid"]; $query = "select * from item_view where uid=:uid"; $results = $pdo->prepare($query); $results->execute(array(':uid' => $uid)); $dataArray = array(); while ($row = $results->fetch(PDO::FETCH_ASSOC)) { $dataArray[] = $row; }
30
1
23
45
67
89
1011
12
1
23
45
67
89
1011
1213
1415
16
echo json_encode($dataArray); }?>
Line 7 is commented out, but if this were uncommented, this would produce the following output:[{"uid":6,"name":"Eric Raymond","username":"esr","division":"div2","department":"deptC","iid":5,"description":"item5","justification":"justification5","origin_department":"deptC","created_at":"2012-01-04 10:00:58.822101","updated_at":"2012-01-04 10:00:58.822101"},{"uid":6,"name":"Eric Raymond","username":"esr","division":"div2","department":"deptC","iid":6,"description":"item6","justification":"justification6","origin_department":"deptC","created_at":"2012-01-04 10:00:58.825481","updated_at":"2012-01-04 10:00:58.825481"},{"uid":6,"name":"Eric Raymond","username":"esr","division":"div2","department":"deptC","iid":7,"description":"item7","justification":"justification7","origin_department":"deptD","created_at":"2012-01-04 10:00:58.8306","updated_at":"2012-01-04 10:00:58.8306"}]
Lines 2-3 just obtain a connection to the database. The if statement defined on lines 1-18 will query the database and write the output to the screen in JSON format. Line 10 sets up a query where we specify the user's id to limit the rows returned to just the records that this particular user is allowed to edit. Lines 11 and 12 use prepare/execute to protect against SQL Injection attacks.
Creating panel in GWT to view items
Once a user, other than the administrator, logs in, he/she should be presented with a view of their current items. This view should also provide a way to add new items. Let's start with generating a panel that can view a user's current items.
Since we will pass the data in JSON format, we can add a new class, Item.java, that contains the JSNI functions needed for reading the data into a JsArray. Then, we can create an inner class MyItem that will be used for the ArrayList that we need to get the data into. We also can add a method called “showUserItems” that can be called from within the onResponseReceived() method within the getRequest() method. The showUserItems() method will populate a CellTable with the items.
To create Item.java, right-click on the src.test.client and choose New=>Class. Here is the contents of Item.java:
{ protected Item() {} public final native int getUserID() /*-{ return this.uid; }-*/; public final native String getName() /*-{ return this.name; }-*/; public final native String getUsername() /*-{ return this.username; }-*/; public final native String getDivision() /*-{ return this.division; }-*/; public final native String getDepartment() /*-{ return this.department; }-*/; public final native int getItemID() /*-{ return this.iid; }-*/; public final native String getDescription() /*-{ return this.description; }-*/; public final native String getJustification() /*-{ return this.justification; }-*/; public final native String getOriginalDeparment() /*-{ return this.origin_department; }-*/; public final native Date getCreatedAt() /*-{ return this.created_at; }-*/; public final native Date getUpdatedAt() /*-{ return this.updated_at; }-*/;}
There is nothing fancy going on with this class. Let's look at the modified DemoProj3.java file next:
public class DemoProj3 implements EntryPoint, ClickHandler{ VerticalPanel mainPanel = new VerticalPanel(); String baseURL = "http://localhost/demo-proj3/"; ArrayList<MyUser> users = new ArrayList<MyUser>(); JsArray<User> jsonData; VerticalPanel loginPanel = new VerticalPanel(); TextBox usernameBox; PasswordTextBox passBox; Button loginButton = new Button("Login"); int user_id; private static class MyUser { private int id; private String name; private String username; private String division; private String department; public MyUser(int id, String name, String username, String division, String department) { this.id = id; this.name = name; this.username = username; this.division = division; this.department = department; } } private static class MyItem { private int uid; private String name; private String username; private String division; private String department;
33
6
78
910
1112
1314
1516
1718
1920
2122
2324
2526
2728
2930
3132
3334
3536
3738
3940
4142
4344
4546
4748
4950
5152
5354
5556
5758
5960
6162
63
private int iid; private String description; private String justification; private String origin_department; private Date created_at; private Date updated_at; public MyItem(int uid, String name, String user, String div, String dept, int iid, String desc, String just, String orig_dept, Date create, Date update) { this.uid = uid; this.name = name; username = user; division = div; department = dept; this.iid = iid; description = desc; justification = just; origin_department = orig_dept; created_at = create; updated_at = update; } } public void onClick(ClickEvent e) { Object source = e.getSource(); if (source == loginButton) { String encData = URL.encode("username") + "=" + URL.encode(usernameBox.getText()) + "&"; encData += URL.encode("password") + "=" + URL.encode(passBox.getText()); String url = baseURL + "login.php"; postRequest(url,encData,"login"); } } public void onModuleLoad() { RootPanel.get().add(mainPanel); init(); } private void getRequest(String url, final String requestName) { final RequestBuilder rb = new RequestBuilder(RequestBuilder.GET,url); try { rb.sendRequest(null, new RequestCallback() { public void onError(final Request request, final Throwable exception) { Window.alert(exception.getMessage()); } public void onResponseReceived(final Request request, final Response response) { if (requestName.equals("getUsers")) {
34
64
6566
6768
6970
7172
7374
7576
7778
7980
8182
8384
8586
8788
8990
9192
9394
9596
9798
99100
101102
103104
105106
107108
109110
111112
113114
115116
117118
119120
121
showUsers(response.getText()); } } }); } catch (final Exception e) { Window.alert(e.getMessage()); } } // end getRequest() private void postRequest(String url, String data, final String requestName) { final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,url); rb.setHeader("Content-type", "application/x-www-form-urlencoded"); try { rb.sendRequest(data, new RequestCallback() { public void onError(final Request request, final Throwable exception) { Window.alert(exception.getMessage()); } public void onResponseReceived(final Request request, final Response response) { if (requestName.equals("login")) { String resp = response.getText().trim(); user_id = Integer.parseInt(resp); if (user_id < 1) { usernameBox.setText(""); passBox.setText(""); } else if (user_id == 1) { mainPanel.clear(); String url = baseURL + "showUsers.php"; getRequest(url,"getUsers"); } else { mainPanel.clear(); String encData = URL.encode("uid") + "=" + URL.encode("" + user_id); String url = baseURL + "showUserItems.php"; postRequest(url,encData,"getUserItems"); } } // ends if (requestName.equals("login")) else if (requestName.equals("getUserItems")) { Window.alert(response.getText()); } // ends else if requestName is "getUserItems" } // onResponseReceived() ends }); // new RequestCallback ends } catch (final Exception e) { Window.alert(e.getMessage()); } } // postRequest() ends private void showUsers(String responseText)
35
122
123124
125126
127128
129130
131132
133134
135136
137138
139140
141142
143144
145146
147148
149150
151152
153154
155156
157158
159160
161162
163164
165166
167168
169170
171172
173174
175176
177178
179
{ jsonData = getData(responseText); User user = null; for (int i = 1; i < jsonData.length(); i++) { user = jsonData.get(i); users.add(new MyUser(user.getID(), user.getName(),user.getUsername(),user.getDivision(), user.getDepartment())); } CellTable<MyUser> table = new CellTable<MyUser>(); TextColumn<MyUser> idColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) { return "" + user.id; } }; TextColumn<MyUser> nameColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) { return user.name; } }; TextColumn<MyUser> usernameColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) { return user.username; } }; TextColumn<MyUser> divColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) { return user.division; } }; TextColumn<MyUser> deptColumn = new TextColumn<MyUser>() { @Override public String getValue(MyUser user) { return user.department; } }; table.addColumn(idColumn,"id number"); table.addColumn(nameColumn,"Name"); table.addColumn(usernameColumn,"Username");
36
180
181182
183184
185186
187188
189190
191192
193194
195196
197198
199200
201202
203204
205206
207208
209210
211212
213214
215216
217218
219220
221222
223224
225226
227228
229230
231232
233234
235236
237
table.addColumn(divColumn,"Division"); table.addColumn(deptColumn,"Department"); table.setRowCount(users.size(),true); table.setRowData(0,users); mainPanel.add(table); } // end showUsers() private JsArray<User> getData(String json) { return JsonUtils.safeEval(json); } private void init() { Label usernameLabel = new Label("Username: "); usernameBox = new TextBox(); HorizontalPanel userRow = new HorizontalPanel(); userRow.add(usernameLabel); userRow.add(usernameBox); loginPanel.add(userRow); Label passLabel = new Label("Password"); passBox = new PasswordTextBox(); HorizontalPanel passRow = new HorizontalPanel(); passRow.add(passLabel); passRow.add(passBox); loginPanel.add(passRow); loginButton.addClickHandler(this); loginPanel.add(loginButton); mainPanel.add(loginPanel); }}
The new lines are shown shaded. The are line 24, line 37, lines 57-88, line 151, line 152, line 156, lines 163-166 and lines 169-171.
Line 24 just adds an import statement for the Date class. Line 37 defines an instance variable, user_id, to hold the id of the logged in user.
Lines 57-88 define the inner class, MyItem. This is the class that will be used to hold the data when it is transferred into an ArrayList from a JsArray.
Line 151 and line 152 have been changed to use the instance variable, user_id instead of id. Line 156 has also been changed to use the instance variable, user_id instead of id.
Lines 163-166 have been modified so that we can send the user's id via POST to the PHP script. Line 166 calls the postRequest() method and passed the user's id.
Lines 169-171 respond to the last postRequest() call. At this point, all that is done is to show the JSON formatted data that is returned in an alert window.
The next step is to take the JSON formatted data and read this into a JsArray. Then, the data will be transferred from the JsArray<Item> to an ArrayList<MyItem>. The ArrayList<MyItem> will be used to populate a CellTable. Here are the portions of the code that get changed:
37
238
239240
241242
243244
245246
247248
249250
251252
253254
255256
257258
259260
261262
263264
265266
// lines above unmodified
public class DemoProj3 implements EntryPoint, ClickHandler{ VerticalPanel mainPanel = new VerticalPanel(); String baseURL = "http://localhost/demo-proj3/"; ArrayList<MyUser> users = new ArrayList<MyUser>(); ArrayList<MyItem> items = new ArrayList<MyItem>(); JsArray<User> jsonData; JsArray<Item> jsonItemData; VerticalPanel loginPanel = new VerticalPanel(); TextBox usernameBox; PasswordTextBox passBox; Button loginButton = new Button("Login"); int user_id;// lines in between here unmodified
// lines in between here unmodified
private void postRequest(String url, String data, final String requestName) { final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,url); rb.setHeader("Content-type", "application/x-www-form-urlencoded"); try { rb.sendRequest(data, new RequestCallback() { public void onError(final Request request, final Throwable exception) { Window.alert(exception.getMessage()); } public void onResponseReceived(final Request request, final Response response) { if (requestName.equals("login")) { String resp = response.getText().trim(); user_id = Integer.parseInt(resp); if (user_id < 1) { usernameBox.setText(""); passBox.setText(""); } else if (user_id == 1) { mainPanel.clear(); String url = baseURL + "showUsers.php"; getRequest(url,"getUsers"); } else { mainPanel.clear(); String encData = URL.encode("uid") + "=" + URL.encode("" + user_id); String url = baseURL + "showUserItems.php"; postRequest(url,encData,"getUserItems"); } } // ends if (requestName.equals("login"))
38
2627
2829
3031
3233
3435
3637
3839
133134
135136
137138
139140
141142
143144
145146
147148
149150
151152
153154
155156
157158
159160
161162
163164
165166
167168
169170
else if (requestName.equals("getUserItems")) { showUserItems(response.getText()); } // ends else if requestName is "getUserItems" } // onResponseReceived() ends }); // new RequestCallback ends } catch (final Exception e) { Window.alert(e.getMessage()); } } // postRequest() ends private void showUsers(String responseText)// code for showUsers() not shown // but is unmodified } // end showUsers() private void showUserItems(String responseText) { jsonItemData = getItemData(responseText); Item item = null; for (int i = 0; i < jsonItemData.length(); i++) { item = jsonItemData.get(i); items.add(new MyItem(item.getUserID(), item.getName(), item.getUsername(), item.getDivision(),item.getDepartment(), item.getItemID(),item.getDescription(), item.getJustification(),item.getOriginalDeparment(), item.getCreatedAt(),item.getUpdatedAt())); } CellTable<MyItem> table = new CellTable<MyItem>(); TextColumn<MyItem> nameColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.name; } }; TextColumn<MyItem> origDeptColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.origin_department; } }; table.addColumn(nameColumn,"Name"); table.addColumn(origDeptColumn,"Orig Dept"); table.setRowCount(items.size(),true); table.setRowData(0,items); mainPanel.add(table); } // end showUserItems() private JsArray<User> getData(String json) { return JsonUtils.safeEval(json); } private JsArray<Item> getItemData(String json) {
39
171
172173
174175
176177
178179
180181
245246
247248
249250
251252
253254
255256
257258
259260
261262
263264
265266
267268
269270
271272
273274
275276
277278
279280
281282
283284
285286
287288
289
return JsonUtils.safeEval(json); } private void init() { Label usernameLabel = new Label("Username: "); usernameBox = new TextBox(); HorizontalPanel userRow = new HorizontalPanel(); userRow.add(usernameLabel); userRow.add(usernameBox); loginPanel.add(userRow); Label passLabel = new Label("Password"); passBox = new PasswordTextBox(); HorizontalPanel passRow = new HorizontalPanel(); passRow.add(passLabel); passRow.add(passBox); loginPanel.add(passRow); loginButton.addClickHandler(this); loginPanel.add(loginButton); mainPanel.add(loginPanel); }}
Large amounts of code are omitted because those lines are unchanged from the previous version. The new lines are shown shaded. The new lines are line 32, line 34, line 172, lines 246-283 and lines 288-291.
Line 32 adds the instance variable of type ArrayList<MyItem> called items. This is where we will store the item data that will be used within the GWT program.
Line 34 adds the instance variable of type JsArray<Item> called jsonItemData. This is where we store the item when we initially read in the JSON data.
Line 172 calls the new method showUserItems(). The showUserItems() method is defined on lines 246-283. On line 248, we call the getItemData() method. This will read the JSON data into the JsArray<Item>. Lines 249-258 are used to read the data from the JsArray<Item> into the ArrayList<MyItem> called items. This puts the data into a format that can be easily used with the GWT program. For example, this is the format needed to populate a CellTable.
Line 259 constructs the CellTable object that will be used to display the items. Lines 260-268 create a column that will be used to include the user's name. Lines 269-277 create a column that will be used to include the originating department. Although we will eventually show more columns, this is good enough to see if things are working correctly.
Line 278 and line 279 add the columns for the name and the originating department, respectively, into the CellTable.
Line 280 sets the row count for the CellTable, and line 281 adds the table to the main panel.
40
290
291292
293294
295296
297298
299300
301302
303304
305306
307308
309310
Lines 288-291 define the getItemData() method. All this method does is call JsonUtils.safeEval() to safely evaluate the JSON-formatted data, and read the data into a JsArray<Item>.
Once you confirm that the CellTable is working correctly, we can add in more of the columns.
Handling date fields
When we create the JSON data with our PHP script, it turns out that we have a problem with date fields. Even though the data is stored as type timestamp, JSON has no format for dates. This means that the dates are passed as strings. This can be a problem when putting the data into the GWT CellTable. The strings that are passed to the GWT application cannot be properly converted into java.util.Date objects.
One way to deal with this is to select the data from the database and reformat the date fields as UNIX timestamps. Passing a UNIX timestamp as a String allows us to eventually get a java.util.Date object. Here is the basic process:
1. Convert the postgresql date value into a UNIX timestamp.
2. Use the PHP function json_encode() to pass this UNIX timestamp as a string.
3. In GWT read the UNIX timestamp as a java.lang.String.
4. Use Double.parseDouble() to convert the String into a double.
5. Multiply that double by 1,000.
6. Round the resulting double using Math.round() to get a long.
7. Use the java.util.Date constructor that takes a long as the argument.
As an example, suppose the value stored in the database for a date is:2012-01-02 9:30:21.1278
This can be converted to a UNIX timestamp using the date_part() function with 'epoch' as the first argument, and the date as the second argument. This can be visualized inside psql in the following way:
That code assumes that create is the name of the parameter that holds the created_at value.
To make these changes, let's start by changing some of our SQL scripts. First, let's change “createTables.sql”:
-- createTables.sql
-- create sequencesdrop sequence if exists user_id_seq cascade;create sequence user_id_seq;drop sequence if exists item_id_seq cascade;create sequence item_id_seq;
-- create tablesdrop table if exists users cascade;create table users( id integer not null default nextval('user_id_seq'), name text, username text unique, password text, enc_pass text, salt text, division text, department text, primary key (id));drop table if exists items cascade;create table items( id integer not null default nextval('item_id_seq'), description text, justification text, origin_department text, created_at timestamp default now(), updated_at timestamp default now(), num_low integer default 0, num_med integer default 0, num_high integer default 0, primary key (id));drop table if exists items_users cascade;create table items_users( user_id integer references users(id), item_id integer references items(id) on delete cascade, unique (user_id,item_id));
The modified lines are lines 28 and 29. This just sets a default value for both timestamp
42
1
23
45
67
89
1011
1213
1415
1617
1819
2021
2223
2425
2627
2829
3031
3233
3435
3637
3839
40
fields. This will make it so that we don't have to insert the values explicitly when adding an item.
Let's also modify the “itemFuncs.sql” script:
-- itemFuncs.sql
create or replace function addItemUser(_uid integer, _id integer) returns boolean as $func$ begin insert into items_users (user_id, item_id) values (_uid, _id); return 't'; end; $func$ language 'plpgsql';
create or replace function addOtherEditors(_others text, _id integer) returns boolean as $func$ declare v_id integer; my_array text[]; begin my_array := string_to_array(_others,','); for i in 1..array_length(my_array,1) loop v_id := cast (my_array[i] as integer); perform addItemUser(v_id,_id); end loop; return 't'; end; $func$ language 'plpgsql'; create or replace function addItem(_uid integer, _desc text, _just text, _others text) returns boolean as $func$ declare rec record; rec2 record; div text; chair_id integer; it_id integer; begin select into rec * from users where id = _uid; if not found then return 'f'; else insert into items (description,justification, origin_department) values (_desc, _just, rec.department); it_id := currval('item_id_seq'); div := rec.division; select into rec2 * from users where department=div;
43
1
234
56
78
910
1112
1314
1516
1718
1920
2122
2324
2526
2728
2930
3132
3334
3536
3738
3940
4142
4344
4546
4748
4950
5152
chair_id := rec2.id; perform addItemUser(_uid, it_id); if rec.id <> chair_id then perform addItemUser(chair_id, it_id); end if; if _others <> '' then perform addOtherEditors(_others,it_id); end if; return 't'; end if; end; $func$ language 'plpgsql';
The changes to this file are on lines 47-49. This just reflects the simpler insert statement due to the default values for the created_at and updated_at fields.
Finally, we can modify “makeAll.sql” to load all of the SQL scripts we have at this point:
In the GWT project, we can start by modifying “Item.java” so that it reads in the date fields as Strings.
package test.client;
import com.google.gwt.core.client.JavaScriptObject;class Item extends JavaScriptObject { protected Item() {} public final native int getUserID() /*-{ return this.uid; }-*/; public final native String getName() /*-{ return this.name; }-*/; public final native String getUsername() /*-{ return this.username; }-*/; public final native String getDivision() /*-{ return this.division; }-*/; public final native String getDepartment()
44
53
5455
5657
5859
6061
6263
6465
1
23
45
67
8
1
23
45
67
89
1011
1213
1415
1617
1819
2021
2223
24
/*-{ return this.department; }-*/; public final native int getItemID() /*-{ return this.iid; }-*/; public final native String getDescription() /*-{ return this.description; }-*/; public final native String getJustification() /*-{ return this.justification; }-*/; public final native String getOriginalDeparment() /*-{ return this.origin_department; }-*/; public final native String getCreatedAt() /*-{ return this.created_at; }-*/; public final native String getUpdatedAt() /*-{ return this.updated_at; }-*/;}
The only lines that are modified are lines 44 and 48. The return type has been changed to String, instead of Date. Also, the import statement for java.util.Date has been removed.
Now, we can modify “DemoProj3.java” in our GWT application so that we can see how a column that has Date data can be included in a CellTable.
// lines 1-24 unmodifiedimport com.google.gwt.cell.client.DateCell;import com.google.gwt.user.cellview.client.Column;// lines 27-60 unmodified private static class MyItem { private int uid; private String name; private String username; private String division; private String department; private int iid; private String description; private String justification; private String origin_department; private Date created_at; private Date updated_at; public MyItem(int uid, String name, String user, String div, String dept, int iid, String desc, String just, String orig_dept,
45
25
2627
2829
3031
3233
3435
3637
3839
4041
4243
4445
4647
4849
5051
52
25
26
6162
6364
6566
6768
6970
7172
7374
7576
77
String create, String update) { this.uid = uid; this.name = name; username = user; division = div; department = dept; this.iid = iid; description = desc; justification = just; origin_department = orig_dept; double numdate = Double.parseDouble(create); numdate = numdate*1000; created_at = new Date(Math.round(numdate)); numdate = Double.parseDouble(update)*1000; updated_at = new Date(Math.round(numdate)); } }//// lines 96-273 are unmodified// TextColumn<MyItem> origDeptColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.origin_department; } }; DateCell createDateCell = new DateCell(); Column<MyItem,Date> createDateColumn = new Column<MyItem, Date>(createDateCell) { @Override public Date getValue(MyItem item) { return (Date)item.created_at; } }; table.addColumn(nameColumn,"Name"); table.addColumn(origDeptColumn,"Orig Dept"); table.addColumn(createDateColumn,"Created"); table.setRowCount(items.size(),true); table.setRowData(0,items); mainPanel.add(table); } // end showUserItems() private JsArray<User> getData(String json) { return JsonUtils.safeEval(json); }// Lines 304 on are unmodified
The modified lines are shaded. The modified lines are lines 25-26, line 78, lines 89-93, lines 284-292 and line 295.
Lines 25 and 26 import the new classes needed for to create a CellTable column that can
46
78
7980
8182
8384
8586
8788
8990
9192
9394
95
274275
276277
278279
280281
282283
284285
286287
288289
290291
292293
294295
296297
298299
300301
302303
hold a java.util.Date object.
Line 78 is modified by changing the variable type from Date to String for both the create and update parameters. This is because we are now passing the data as Strings.
Lines 89-93 show the new lines inside the MyItem constructor. For each date field, the passed String is converted into a double, multiplied by 1000, and rounded into the nearest long value. That long value is used to construct a Date object.
Lines 284-292 construct the column that the created_at dates will be inserted into. This shows how to insert values other than Strings. The other columns are TextColumns since they hold String data.
Finally, on line 295, we add the newly created column to the table.
Here is another modification to “DemoProj3.java” to add a column that shows the updated_at date:
DateCell createDateCell = new DateCell(); Column<MyItem,Date> createDateColumn = new Column<MyItem, Date>(createDateCell) { @Override public Date getValue(MyItem item) { return item.created_at; } }; DateCell updateDateCell = new DateCell(); Column<MyItem,Date> updateDateColumn = new Column<MyItem, Date>(updateDateCell) { @Override public Date getValue(MyItem item) { return item.updated_at; } }; table.addColumn(nameColumn,"Name"); table.addColumn(origDeptColumn,"Orig Dept"); table.addColumn(createDateColumn,"Created"); table.addColumn(updateDateColumn,"Modified"); table.setRowCount(items.size(),true); table.setRowData(0,items); mainPanel.add(table); } // end showUserItems() private JsArray<User> getData(String json) { return JsonUtils.safeEval(json); }// code below unmodified
Lines 293-302 create the column for inserting the update_at data. This is very similar to the column created for the created_at data. Line 306 just adds this column into the table.
47
284285
286287
288289
290291
292293
294295
296297
298299
300301
302303
304305
306307
308309
310311
312313
314
Making the CellTable sortable
The extra effort to get the columns for the created_at and updated_at fields to be entered as Date objects, is to make those columns sortable. This is so you can see which items have been modified most recently, for example. So, let's make additions to “DemoProj3.java” to make those columns sortable.
public class DemoProj3 implements EntryPoint, ClickHandler{ VerticalPanel mainPanel = new VerticalPanel();//// lines 35-296 are unmodified// DateCell updateDateCell = new DateCell(); Column<MyItem,Date> updateDateColumn = new Column<MyItem, Date>(updateDateCell) { @Override public Date getValue(MyItem item) { return item.updated_at; } }; ListDataProvider<MyItem> dataProvider = new ListDataProvider<MyItem>();
48
1
23
45
67
89
1011
1213
1415
1617
1819
2021
2223
2425
2627
2829
30
313233
34
297298
299300
301302
303304
305306
307308
dataProvider.addDataDisplay(table); List<MyItem> list = dataProvider.getList(); for (MyItem itm : items) { list.add(itm); } makeSortableDateColumn(table,createDateColumn,list,"create"); makeSortableDateColumn(table,updateDateColumn,list,"update"); table.addColumn(nameColumn,"Name"); table.addColumn(origDeptColumn,"Orig Dept"); table.addColumn(createDateColumn,"Created"); table.addColumn(updateDateColumn,"Modified"); table.setRowCount(items.size(),true); table.setRowData(0,items); mainPanel.add(table); } // end showUserItems() private JsArray<User> getData(String json) { return JsonUtils.safeEval(json); } private JsArray<Item> getItemData(String json) { return JsonUtils.safeEval(json); } private void makeSortableDateColumn(CellTable<MyItem> table, Column<MyItem,Date> col, List<MyItem> list, final String type) { col.setSortable(true); ListHandler<MyItem> handler = new ListHandler<DemoProj3.MyItem>(list); handler.setComparator(col, new Comparator<DemoProj3.MyItem>() { @Override public int compare(DemoProj3.MyItem i1, DemoProj3.MyItem i2) { if (i1 == i2) { return 0; } if (i1 != null) { if (type.equals("create")) { return (i2 != null) ? i1.created_at.compareTo(i2.created_at) : 1; } else if (type.equals("update")) { return (i2 != null) ? i1.updated_at.compareTo(i2.updated_at) : 1; } } return -1; } }); table.addColumnSortHandler(handler); } private void init() {// Lines 365 on are unmodified
49
309
310311
312313
314315
316317
318319
320321
322323
324325
326327
328329
330331
332333
334335
336337
338339
340341
342343
344345
346347
348349
350351
352353
354355
356357
358359
360361
362363
364
The new lines are shown shaded. These are lines 27-30, lines 307-315 and lines 332-362.
Lines 27-30 just add the import statements for the new classes needed. Line 27 includes the Comparator class. This is the class used to make comparisons for sorting purposes. We will use this class on line 340, to make allow sorting on a column. Line 28 adds the List class. This is used on line 310 when we create a List that holds the same data as the CellTable. Since a List object is sortable, we can sort the data. If we can tie the List to the CellTable, then the CellTable will be sorted as well. On line 30 we import the ListDataProvider class. This is the class that is used to tie the List to the CellTable. On line 29 we import the ListHandler class. This is the class that is used to attach a method for handling the sorting, when we click on a column.
Lines 307-308 construct a ListDataProvider<MyItem> called dataProvider. On line 309 this object is connected to the CellTable. Lines 310-313 construct and populate a List for dataProvider that contains all the same data as the CellTable. These steps connect a sortable object to the CellTable.
Lines 314 and 315 call the makeSortableDateColumn() method. This method is defined on lines 332-362. This method will add a ListHandler to a column and make that column sortable. This method is geared specifically to handle a column of type Column<MyItem, Date>.
Inside the makeSortableDateColumn() method, on line 336 we set the column to be sortable. However, this by itself does not make the column sort, because the program needs to know how to compare two objects in that column. To do that, we need to create a ListHandler that will be called when you click on the column header. Lines 337-338 just construct a ListHandler object that is tied to the List constructed on line 310. Lines 340-360 just create a new Comparator object to use to sort the data. A Comparator object has a compare() method, and this is defined on lines 343-359. On lines 345-347, an if statement that tests to see if the objects are the same object is defined. This will return the number zero if this is the case (that is they are equal). On lines 350-351 and on lines 354-355, a ternary operator is used to compare the Date objects. A ternary operator works like this:(a < b) ? a : b
If the (a < b) is true, this returns a. Otherwise, it returns b. So, in our case, the program check to see if the objects (i1 and i2) are null. If both are not null, the program returns i1.created_at.compareTo(i2.created_at) and returns the result. This works because the Date object has a compareTo() method that performs this comparison. A similar thing is done on lines 354-355, but the updated_at value is used instead.
Sorting by a TextColumn
We can also sort by columns that contain Strings. Here are the modifications needed to sort
50
by the origin_department field column.
\\ Lines 1-315 are unmodified makeSortableTextColumn(table,origDeptColumn,list,"orig_dept"); table.addColumn(nameColumn,"Name"); table.addColumn(origDeptColumn,"Orig Dept"); table.addColumn(createDateColumn,"Created"); table.addColumn(updateDateColumn,"Modified"); table.setRowCount(items.size(),true); table.setRowData(0,items); mainPanel.add(table); } // end showUserItems() private JsArray<User> getData(String json) { return JsonUtils.safeEval(json); }//// Lines 329-361 are unmodified// table.addColumnSortHandler(handler); } private void makeSortableTextColumn(CellTable<MyItem> table, TextColumn<MyItem> col, List<MyItem> list, final String type) { col.setSortable(true); ListHandler<MyItem> handler = new ListHandler<DemoProj3.MyItem>(list); handler.setComparator(col, new Comparator<DemoProj3.MyItem>() { @Override public int compare(DemoProj3.MyItem i1, DemoProj3.MyItem i2) { if (i1 == i2) { return 0; } if (i1 != null) { if (type.equals("orig_dept")) { return (i2 != null) ? i1.origin_department.compareTo( i2.origin_department) : 1; } } return -1; } }); table.addColumnSortHandler(handler); } private void init() {// Lines 394 on are unmodified
The new lines are line 316 and lines 364-391. Line 316 just calls the makeSortableTextColumn() method. That method is defined on lines 364-391. This
51
316317
318319
320321
322323
324325
326327
328
362363
364365
366367
368369
370371
372373
374375
376377
378379
380381
382383
384385
386387
388389
390391
392393
method is very similar to the makeSortableDateColumn() method. In fact, these methods could be combined if it were not for the fact that they work on a different type of column. The makeSortableDateColumn() method uses a Column<MyItem, Date>, while the makeSortableTextColumn() method uses a TextColumn<MyItem>. Otherwise, they do just about the same thing.
Allowing a user to add a new item
Let's start by adding a Button on top of the CellTable that displays the items. To give us more space, let's modify “DemoProj3.css”. Simply delete all the lines. We will add some lines later to clean up buttons and text boxes. But for now, just delete all the lines.
Run the GWT application in the browser to see how the empty lines below the heading1 element are gone.
Inside “DemoProj3.java”, make the following changes:
// Lines 1-31 are unmodifiedpublic class DemoProj3 implements EntryPoint, ClickHandler{ VerticalPanel mainPanel = new VerticalPanel(); String baseURL = "http://localhost/demo-proj3/"; ArrayList<MyUser> users = new ArrayList<MyUser>(); ArrayList<MyItem> items = new ArrayList<MyItem>(); JsArray<User> jsonData; JsArray<Item> jsonItemData; VerticalPanel loginPanel = new VerticalPanel(); TextBox usernameBox; PasswordTextBox passBox; Button loginButton = new Button("Login"); int user_id; Button addItemButton = new Button("Add new item");//// Lines 47- 256 are unmodified // private void showUserItems(String responseText) { jsonItemData = getItemData(responseText); Item item = null; for (int i = 0; i < jsonItemData.length(); i++) { item = jsonItemData.get(i); items.add(new MyItem(item.getUserID(), item.getName(), item.getUsername(), item.getDivision(),item.getDepartment(), item.getItemID(),item.getDescription(), item.getJustification(),item.getOriginalDeparment(), item.getCreatedAt(),item.getUpdatedAt())); } CellTable<MyItem> table = new CellTable<MyItem>(); TextColumn<MyItem> nameColumn = new TextColumn<MyItem>() {
52
32
3334
3536
3738
3940
4142
4344
4546
257
258259
260261
262263
264265
266267
268269
270271
272273
@Override public String getValue(MyItem item) { return item.name; } }; TextColumn<MyItem> origDeptColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.origin_department; } }; DateCell createDateCell = new DateCell(); Column<MyItem,Date> createDateColumn = new Column<MyItem, Date>(createDateCell) { @Override public Date getValue(MyItem item) { return item.created_at; } }; DateCell updateDateCell = new DateCell(); Column<MyItem,Date> updateDateColumn = new Column<MyItem, Date>(updateDateCell) { @Override public Date getValue(MyItem item) { return item.updated_at; } }; ListDataProvider<MyItem> dataProvider = new ListDataProvider<MyItem>(); dataProvider.addDataDisplay(table); List<MyItem> list = dataProvider.getList(); for (MyItem itm : items) { list.add(itm); } makeSortableDateColumn(table,createDateColumn,list,"create"); makeSortableDateColumn(table,updateDateColumn,list,"update"); makeSortableTextColumn(table,origDeptColumn,list,"orig_dept"); table.addColumn(nameColumn,"Name"); table.addColumn(origDeptColumn,"Orig Dept"); table.addColumn(createDateColumn,"Created"); table.addColumn(updateDateColumn,"Modified"); table.setRowCount(items.size(),true); table.setRowData(0,items); addItemButton.addClickHandler(this); mainPanel.add(addItemButton); mainPanel.add(table); } // end showUserItems() private JsArray<User> getData(String json) { return JsonUtils.safeEval(json);
53
274
275276
277278
279280
281282
283284
285286
287288
289290
291292
293294
295296
297298
299300
301302
303304
305306
307308
309310
311312
313314
315316
317318
319320
321322
323324
325326
327328
329330
331
}// Lines 333 on are unmodified
So far, only three lines are new. These are line 46 and lines 325-326. Line 46 just adds an instance variable addItemButton that will be the button that is clicked on to add a new item.
Line 325 just makes it so that this application to respond to the ClickEvent that is generated when the addItemButton is clicked on. Line 326 adds this button right above the CellTable.
After verifying that the button shows up, modify “DemoProj3.java” to check to see if the application responds to clicking on that button:
The new lines, shown shaded, just show an Alert box when the button is clicked. Next, let's put up some text boxes and a button that will be used to insert a new item into the database.
First, let's modify “DemoProj3.java” so that it has some new instance variables to perform this task:
public class DemoProj3 implements EntryPoint, ClickHandler{ VerticalPanel mainPanel = new VerticalPanel(); String baseURL = "http://localhost/demo-proj3/"; ArrayList<MyUser> users = new ArrayList<MyUser>(); ArrayList<MyItem> items = new ArrayList<MyItem>(); JsArray<User> jsonData; JsArray<Item> jsonItemData; VerticalPanel loginPanel = new VerticalPanel(); TextBox usernameBox; PasswordTextBox passBox; Button loginButton = new Button("Login"); int user_id; Button addItemButton = new Button("Add new item"); TextArea descBox; TextArea justBox; CheckBox deptA_cbox; CheckBox deptB_cbox; CheckBox deptC_cbox; CheckBox deptD_cbox; Button submitItemButton = new Button("Submit item"); private static class MyUser // lines below unmodified so far
The new lines are shown shaded. The new lines are lines 31-33 and lines 50-56. Line 31 just imports the TextArea class. We will use this class to enter long text data like the description and justification for an item. Line 32 imports the CheckBox class. This class can be used to select additional users that can edit the item. Line 33 imports the HTML class. This class is used to add text that functions as headings. Lines 50-56 add in the instance variables that we will use to input the item information needed to insert a new item.
Now, let's add these text areas, check boxes and button to the mainPanel, after first clearing the mainPanel.
// Lines 1-110 unchanged
55
18
1920
2122
2324
2526
2728
2930
3132
33
343536
3738
3940
4142
4344
4546
4748
4950
5152
5354
5556
5758
public void onClick(ClickEvent e) { Object source = e.getSource(); if (source == loginButton) { String encData = URL.encode("username") + "=" + URL.encode(usernameBox.getText()) + "&"; encData += URL.encode("password") + "=" + URL.encode(passBox.getText()); String url = baseURL + "login.php"; postRequest(url,encData,"login"); } else if (source == addItemButton) { mainPanel.clear(); showAddItemPanel(); } } public void onModuleLoad() {//// Lines 129-409 are unmodified// private void init() { Label usernameLabel = new Label("Username: "); usernameBox = new TextBox(); HorizontalPanel userRow = new HorizontalPanel(); userRow.add(usernameLabel); userRow.add(usernameBox); loginPanel.add(userRow); Label passLabel = new Label("Password"); passBox = new PasswordTextBox(); HorizontalPanel passRow = new HorizontalPanel(); passRow.add(passLabel); passRow.add(passBox); loginPanel.add(passRow); loginButton.addClickHandler(this); loginPanel.add(loginButton); mainPanel.add(loginPanel); } private void showAddItemPanel() { HTML heading = new HTML("<h2>Enter new item information:</h2>"); mainPanel.add(heading); Label descLabel = new Label("Description"); mainPanel.add(descLabel); descBox = new TextArea(); descBox.setSize("500", "100"); mainPanel.add(descBox); Label justLabel = new Label("Justification"); mainPanel.add(justLabel); justBox = new TextArea(); justBox.setSize("500","100"); mainPanel.add(justBox); HTML addUserHeading = new HTML("<h3>Check additional editors</h3>"); mainPanel.add(addUserHeading); deptA_cbox = new CheckBox("deptA"); deptB_cbox = new CheckBox("deptB");
56
111
112113
114115
116117
118119
120121
122123
124125
126127
128
410411
412413
414415
416417
418419
420421
422423
424425
426427
428429
430431
432433
434435
436437
438439
440441
442443
444445
446
deptC_cbox = new CheckBox("deptC"); deptD_cbox = new CheckBox("deptD"); HorizontalPanel checkboxRow = new HorizontalPanel(); checkboxRow.add(deptA_cbox); checkboxRow.add(deptB_cbox); checkboxRow.add(deptC_cbox); checkboxRow.add(deptD_cbox); mainPanel.add(checkboxRow); submitItemButton.addClickHandler(this); mainPanel.add(submitItemButton); }}
The new lines are shown shaded. They are lines 123-124 and lines 428-457. Line 123 just clears the mainPanel, and line 124 calls the showAddItemPanel() method that is defined on lines 428-457.
Lines 427-457 define the showAddItemPanel() method that displays the input widgets that are used to collect information for adding a new item. Note on lines 434 and 435 how the TextArea object for the item description is created and then sized. A similar thing is done for the item justification TextArea on lines 439 and 440. Lines 430 and 442-443 are used to put some HTML into this panel. HTML objects are used because they are easier to format than Label objects. Lines 445-448 construct the CheckBox objects. Note how the constructor is passed the String that serves as the CheckBox label. On line 455 this application is set as the handler for ClickEvents generated by click on the submitItemButton.
Posting the data to a PHP script that performs the insert
The next thing we have to do is collect the input data and call the postRequest() method to send the data to a PHP script. Let's start by setting up to call the postRequest() method. Here are the changes to “DemoProj3.java”:// LInes 1-110 are unmodified111 public void onClick(ClickEvent e) { Object source = e.getSource(); if (source == loginButton) { String encData = URL.encode("username") + "=" + URL.encode(usernameBox.getText()) + "&"; encData += URL.encode("password") + "=" + URL.encode(passBox.getText()); String url = baseURL + "login.php"; postRequest(url,encData,"login"); } else if (source == addItemButton) { mainPanel.clear(); showAddItemPanel(); } else if (source == submitItemButton) { String encData = URL.encode("description") + "=" + URL.encode(descBox.getText()) + "&"; encData += URL.encode("justification") + "=" +
57
447
448449
450451
452453
454455
456457
458
111
112113
114115
116117
118119
120121
122123
124125
126127
128129
URL.encode(justBox.getText()) + "&"; String checkBoxString = ""; int numBoxesChecked = 0; if (deptA_cbox.getValue()) { numBoxesChecked++; } if (deptB_cbox.getValue()) { numBoxesChecked++; } if (deptC_cbox.getValue()) { numBoxesChecked++; } if (deptD_cbox.getValue()) { numBoxesChecked++; } if (deptA_cbox.getValue()) { checkBoxString += "deptA"; numBoxesChecked--; if (numBoxesChecked > 0) { checkBoxString += ","; } } if (deptB_cbox.getValue()) { checkBoxString += "deptB"; numBoxesChecked--; if (numBoxesChecked > 0) { checkBoxString += ","; } } if (deptC_cbox.getValue()) { checkBoxString += "deptC"; numBoxesChecked--; if (numBoxesChecked > 0) { checkBoxString += ","; } } if (deptD_cbox.getValue()) { checkBoxString += "deptD"; numBoxesChecked--; if (numBoxesChecked > 0) { checkBoxString += ","; } } encData += URL.encode("otherDepts") + "=" + URL.encode(checkBoxString) + "&"; encData += URL.encode("uid") + "=" + URL.encode("" + user_id); String url = baseURL + "createItem.php"; postRequest(url, encData, "createItem"); } // else if source == addItemButton ends } public void onModuleLoad() {//// Lines 182-210 are unmodified// private void postRequest(String url, String data, final String requestName)
58
130
131132
133134
135136
137138
139140
141142
143144
145146
147148
149150
151152
153154
155156
157158
159160
161162
163164
165166
167168
169170
171172
173174
175176
177178
179180
181182
211
212
{ final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,url); rb.setHeader("Content-type", "application/x-www-form-urlencoded"); try { rb.sendRequest(data, new RequestCallback() { public void onError(final Request request, final Throwable exception) { Window.alert(exception.getMessage()); } public void onResponseReceived(final Request request, final Response response) { if (requestName.equals("login")) { String resp = response.getText().trim(); user_id = Integer.parseInt(resp); if (user_id < 1) { usernameBox.setText(""); passBox.setText(""); } else if (user_id == 1) { mainPanel.clear(); String url = baseURL + "showUsers.php"; getRequest(url,"getUsers"); } else { mainPanel.clear(); String encData = URL.encode("uid") + "=" + URL.encode("" + user_id); String url = baseURL + "showUserItems.php"; postRequest(url,encData,"getUserItems"); } } // ends if (requestName.equals("login")) else if (requestName.equals("getUserItems")) { showUserItems(response.getText()); } // ends else if requestName is "getUserItems" else if (requestName.equals("createItem")) { showUserItems(response.getText()); } // ends elseif requestName is "createItem" } // onResponseReceived() ends }); // new RequestCallback ends } catch (final Exception e) { Window.alert(e.getMessage()); } } // postRequest() ends private void showUsers(String responseText) {// Lines 264 on are unmodified
Now, let's create the PHP script, “createItem.php” to see if we are passing the correct data. At this early stage, just place this into “createItem.php”:
<?php
59
213
214215
216217
218219
220221
222223
224225
226227
228229
230231
232233
234235
236237
238239
240241
242243
244245
246247
248249
250251
252253
254255
256257
258259
260261
262263
?>
Run the GWT application, but make sure that FireBug is activated. You will see an error but you should be able to see the “Post” values. Check to see that what you were trying to submit is actually being submitted:
Note how the Post values match what was submitted in the window above. So, things are working from the GWT side.
Now, let's work on the PHP side. We can start by hardcoding the values in the $_POST array. Then, we can take a look at how hard it would be to get the user ids for the additional users that are allowed to edit the item. Here is the file “createItem.php”:
Lines 7-10 hardcode the data to simulate the actual POST event. Line 16 break the $otherDepts string into an array of strings using a comma as the delimiter. Lines 17-24 will obtain the user id for each of the departments. Line 17 sets up the select query to obtain the id. Line 18 prepares this statement for execution. The for loop defined on lines 19-24, will show the department, then run the query for that department. Then, the returned id will be printed. Running this script from the command line shows:
php -f createItem.php
deptA<br />id: 3<br />deptC<br />id: 6<br />
This returns the correct id numbers. This is a good way to solve the problem if we were always going to use PHP. The prepare/execute method prevents SQL injection attacks, and is very efficient when you are calling multiple executes for the same prepared statement. However, since we are assuming that we might not always use PHP, let's see if we can write a plpgsql function that returns a string that contains the id numbers. To do this, let's add to itemFuncs.sql:
// Lines 1-31 are unmodified create or replace function addItem(_uid integer, _desc text, _just text, _others text) returns boolean as $func$
61
3
45
67
89
1011
1213
1415
1617
1819
2021
2223
2425
26
3233
3435
declare rec record; rec2 record; div text; chair_id integer; it_id integer; begin select into rec * from users where id = _uid; if not found then return 'f'; else insert into items (description,justification, origin_department) values (_desc, _just, rec.department); it_id := currval('item_id_seq'); div := rec.division; select into rec2 * from users where department=div; chair_id := rec2.id; perform addItemUser(_uid, it_id); if rec.id <> chair_id then perform addItemUser(chair_id, it_id); end if; if _others <> '' then perform addOtherEditors(_others,it_id); end if; return 't'; end if; end; $func$ language 'plpgsql'; create or replace function makeIDString(_deptString text) returns text as $func$ declare myarray text[]; rec record; _idstring text; _count integer; _idtext text; begin _idstring := ''; myarray := string_to_array(_deptString,','); _count = array_length(myarray,1); for i in 1.._count loop select into rec * from users where department=myarray[i]; if not found then return "error department not found"; else _idtext := trim(to_char(rec.id,'9d')); _idstring := _idstring||_idtext; if i < _count then _idstring := _idstring||','; end if; end if; end loop; return _idstring; end;
62
36
3738
3940
4142
4344
4546
4748
4950
5152
5354
5556
5758
5960
6162
6364
6566
6768
6970
7172
7374
7576
7778
7980
8182
8384
8586
8788
8990
9192
93
$func$ language 'plpgsql';
The new lines are shown shaded. They are lines 67-95. Those lines define the makeIDString() function. The purpose of this function is to return a string that has theuser id numbers for all the other users allowed to edit the item. In the returned string, the id numbers are separated by commas.
Line 77 just sets the string _idstring to an empty string. Line 78 splits the passed string using a comma, and stores the resulting array of strings in myarray. Line 79 just stores the size of the array in _count. Lines 80-91 define a for loop that iterates over the elements in myarray. On line 81 we select the record from the users table that has a department that matches. If no match is found, the if statement on lines 82-83 returns an error message. Otherwise, the id number from that record is converted into a string with the whitespace removed on line 85. Line 86 appends the converted and trimmed id on to the end of _idstring. Lines 87-88 add a comma in between the converted id. Finally, on line 92, the string of id numbers is returned. We can test this new function in the following way:
The new lines are shown shaded. They are lines 16-22. Line 16 creates a SQL select query statement that will call the makeIDString() function. Line 17 prepares that query and line 18 executes that query. Lines 19-20 obtain the id string. Line 21 just echoes this id string to the screen. Here is the run from the command line:
php -f createItem.php3,6
This is the correct string for the hardcoded value on line 9. Now that this works, we can add to “createItem.php” so that it can do insert an item. Recall that the function addItem() is called in this way:select addItem(2,'desc1','just1','3,6')
where 2 is the submitting user's id, 'desc1' is the item description, 'just1' is the item justification, and '3,6' is the string with the additional user id numbers allowed to edit this item. Here is the new version of “createItem.php”:
While running this from the command line, it seems that it did not insert any new records. The problem is that the hardcoded data is not really correct. It turns out that in the original data, the user with id=3, is also from 'deptA'. So, trying to insert the additional
64
24
12
34
56
78
910
1112
1314
1516
1718
1920
2122
2324
2526
27
editors resulted in a violation for the items_users table. The items_users table has a constraint that the combination of user_id, item_id must be unique. This generates an error that prevents any of the insertions to take place.
This shows how important it is to place such constraints on the table, as this prevents duplicate data from being entered. It is easy to see that the user of the GWT application could make this mistake. They might mistakenly choose their own department when setting additional editors. This should be stopped at the database level, because at the GWT level an additional query has to be made.
Since all insertions are prevented when the uniqueness violation occurs, even the items table is not updated. What this means, is that we have to go back and fix the addItemUser() function in the “itemFuncs.sql” script. Here is the new version of “itemFuncs.sql”:
-- itemFuncs.sql
create or replace function addItemUser(_uid integer, _id integer) returns boolean as $func$ declare rec record; begin select into rec * from items_users where user_id=_uid and item_id=_id; if not found then insert into items_users (user_id, item_id) values (_uid, _id); else return 'f'; end if; return 't'; end; $func$ language 'plpgsql';
create or replace function addOtherEditors(_others text, _id integer)// lines 24 on are unmodified
The modified lines are shown shaded. These lines are lines 6-7 and lines 9-16. Lines 6-7 declare a local variable for holding a database table record. Lines 9-10 attempt to return a record where the user_id/item_id pair, have already been entered. If the combination of user_id/item_id don't exist, then the user's id and item's id will be inserted into the items_users table on lines 12-13. If the combination exists, then the function returns False on line 15.
These changes make it so that if someone makes a mistake at the GWT application level and adds their own department to the list of additional editors, only the insertion that would have resulted in a duplicate record would be prevented. All the other insertions into the
65
1
23
45
67
89
1011
1213
1415
1617
1819
20
212223
items_users table would take place.
To make it easier to reset the database with data that is useful to start with, the “itemData.sql” script has been changed to this:
-- itemData.sqlinsert into items (description, justification, origin_department, created_at, updated_at) values ('item1', 'justification1','div1',timestamp '2012-01-01 9:30:22.1135', timestamp '2012-01-03');insert into items_users values(2,currval('item_id_seq'));insert into items_users values(3,currval('item_id_seq'));insert into items_users values(5,currval('item_id_seq'));insert into items (description, justification, origin_department, created_at, updated_at) values ('item2', 'justification2','deptA',timestamp '2012-01-02 10:30:22.1135', timestamp '2012-01-04');insert into items_users values(3,currval('item_id_seq'));insert into items_users values(2,currval('item_id_seq'));insert into items_users values(5,currval('item_id_seq'));insert into items (description, justification, origin_department, created_at, updated_at) values ('item3', 'justification3','deptA',timestamp '2012-01-03 10:30:22.1135', timestamp '2012-01-04');insert into items_users values(3,currval('item_id_seq'));insert into items_users values(2,currval('item_id_seq'));insert into items (description, justification, origin_department, created_at, updated_at) values ('item4', 'justification4','deptB',timestamp '2012-01-04 10:30:22.1135', timestamp '2012-01-05');insert into items_users values(4,currval('item_id_seq'));insert into items_users values(2,currval('item_id_seq'));insert into items_users values(3,currval('item_id_seq'));insert into items (description, justification, origin_department, created_at, updated_at) values ('item5', 'justification5','deptC',timestamp '2012-01-05 10:30:22.1135', timestamp '2012-01-06');insert into items_users values(6,currval('item_id_seq'));insert into items_users values(5,currval('item_id_seq'));insert into items_users values(3,currval('item_id_seq'));insert into items (description, justification, origin_department, created_at, updated_at) values ('item6', 'justification6','deptC',timestamp '2012-01-05 10:30:22.1135', timestamp '2012-01-07');insert into items_users values(6,currval('item_id_seq'));insert into items_users values(5,currval('item_id_seq'));insert into items_users values(4,currval('item_id_seq'));insert into items_users values(7,currval('item_id_seq'));insert into items (description, justification, origin_department, created_at, updated_at) values ('item7', 'justification7','deptD',timestamp '2012-01-05 10:30:22.1135', timestamp '2012-01-07');insert into items_users values(7,currval('item_id_seq'));insert into items_users values(5,currval('item_id_seq'));insert into items_users values(3,currval('item_id_seq'));insert into items_users values(6,currval('item_id_seq'));/*
The insertions that go from line 2 to line 51 basically replace the old insertions that go from line 53-59. This shows you how useful the addItem() function is. But, by doing things this way, we can insert more interesting dates for the created_at and updated_at fields.
Now we can go back and modify “createItem.php”, and try to run the GWT application to insert a new item.
The new lines are shown shaded. They are lines 253-254, line 331, line 333 and lines 403-409. Lines 253-254 change what is done when the “createItem” postRequest is called. Clearing the mainPanel whenever you need to redraw a table is always a good idea. On line 254 we call the method redrawUserItems(), rather than just try to call a postRequest() directly. Calling a postRequest() from with the postRequest() would cause an unwanted recursion. The redrawUserItems()method is defined on lines 403-409.
Line 331 and line 333 are used to clear the mainPanel and clear the ArrayList<MyItem>, respectively, before each time the items CellTable is redrawn.
Lines 403-409 define the redrawUserItems() method. This method just repeats the same instructions as when a nonadministrative user logs in successfully. The key thing is to take the postRequest() call outside of the postRequest() method to avoid recursive calls.
At this point, the CellTable for displaying a user's items only had a few columns just to test things out. Let's add the description, justification, and iid (item id) columns. Here are the changes to “DemoProj3.java”:
The new lines are shown shaded. They are line 34, lines 364-391, line 422 and lines 424-425. Line 34 just adds the import statement for the NumberCell class. This is the class that we need to help construct a CellTable column that contains a number.
Lines 364-391 define additional columns for the CellTable. Lines 364-372 define a column that the description data can be inserted into. Since this is a TextColumn, this is similar to the other columns containing Strings. Lines 373-381 define a column that the justification data can be inserted into. This, like the column for description data is just a TextColumn.
Lines 382-391 define a column for the item id. Since the id is a number, we use a column that is composed of NumberCell objects. On line 382, we construct a NumberCell object. On line 383 we start the construction of a column that has a row type of MyItem and a column type of Number (java.lang.Number). Lines 384 complete the construction of this column.
Lines 422, 424 and 425 just add these columns to the CellTable. Note that the order these are added to the table is the order the columns are displayed in for the CellTable. Here is a view of the CellTable. See Figure 1 on page 74.
73
416
417418
419420
421422
423424
425426
427428
429430
431432
433434
435
Now, let's add the capability to sort by the item id. Here are the changes to “DemoProj3.java” that can do that:
// Lines 1-410 are unmodified ListDataProvider<MyItem> dataProvider = new ListDataProvider<MyItem>(); dataProvider.addDataDisplay(table); List<MyItem> list = dataProvider.getList(); for (MyItem itm : items) { list.add(itm); } makeSortableDateColumn(table,createDateColumn,list,"create"); makeSortableDateColumn(table,updateDateColumn,list,"update"); makeSortableTextColumn(table,origDeptColumn,list,"orig_dept"); makeSortableNumberColumn(table,iidColumn,list,"iid"); table.addColumn(nameColumn,"Name"); table.addColumn(iidColumn,"Item id"); table.addColumn(origDeptColumn,"Orig Dept"); table.addColumn(descColumn,"Description"); table.addColumn(justColumn,"Justification"); table.addColumn(createDateColumn,"Created"); table.addColumn(updateDateColumn,"Modified"); table.setRowCount(items.size(),true); table.setRowData(0,items); addItemButton.addClickHandler(this); mainPanel.add(addItemButton); mainPanel.add(table); } // end showUserItems() private void redrawUserItems() {
Figure 1: Partial screenshot showing user's items sorted by modified date in descending order.
411412
413414
415416
417418
419420
421422
423424
425426
427428
429430
431432
433434
435436
437
481482
final String type) { col.setSortable(true); ListHandler<MyItem> handler = new ListHandler<DemoProj3.MyItem>(list); handler.setComparator(col, new Comparator<DemoProj3.MyItem>() { @Override public int compare(DemoProj3.MyItem i1, DemoProj3.MyItem i2) { if (i1 == i2) { return 0; } if (i1 != null) { if (type.equals("orig_dept")) { return (i2 != null) ? i1.origin_department.compareTo( i2.origin_department) : 1; } } return -1; } }); table.addColumnSortHandler(handler); } private void makeSortableNumberColumn(CellTable<MyItem> table, Column<MyItem,Number> col, List<MyItem> list, final String type) { col.setSortable(true); ListHandler<MyItem> handler = new ListHandler<DemoProj3.MyItem>(list); handler.setComparator(col, new Comparator<DemoProj3.MyItem>() { @Override public int compare(DemoProj3.MyItem i1, DemoProj3.MyItem i2) { if (i1 == i2) { return 0; } if (i1 != null) { if (type.equals("iid")) { return i1.iid - i2.iid; } } return -1; } }); table.addColumnSortHandler(handler); } private void init()536 {// Lines 537 on are unmodified
The new lines are shown shaded. They are line421 and lines 509-534. LIne 421 calls the
75
483
484485
486487
488489
490491
492493
494495
496497
498499
500501
502503
504505
506507
508509
510511
512513
514515
516517
518519
520521
522523
524525
526527
528529
530531
532533
534535
536
makeSortableNumberColumn() method. This method is used to make a numeric column sortable and is defined on lines 509-534.
On line 513, we set the column to be sortable, although this by itself does not make the column sortable until we create a ListHandler that defines the way to perform the sorting. Lines 514-515 attach the handler to the List<MyItem>. Lines 517-532 define the compare() method that will be used to sort the column. Line 527 holds the main part of the compare() method. It simply returns the difference between the iid values for the two items being compared. Line 533 adds the sort handler to the table.
Now, our CellTable of items can be sorted by the item id number.
Editing an existing item
It is usually nice to be able to edit an existing item. Someone may want to change a description or add to the justification for an item. Someone may want to change which additional users are allowed to edit an item. To edit an item, you need to have a way of selecting that item first.
One way to handle selection of an item is to make use of a SelectionModel. Since we want to only edit one item at a time we will make use of the com.google.gwt.view.client.SingleSelectionModel class. We can start by implementing the SingleSelectionModel, and just having a Window.alert() display the item id for the row that is clicked on. The changes to “DemoProj3.java” to do this are shown next:
// Lines 1-31 are unmodifiedimport com.google.gwt.user.client.ui.CheckBox;import com.google.gwt.user.client.ui.HTML;import com.google.gwt.cell.client.NumberCell;import com.google.gwt.view.client.SelectionChangeEvent;import com.google.gwt.view.client.SingleSelectionModel;
public class DemoProj3 implements EntryPoint, ClickHandler{//// Lines 40-331// private void showUserItems(String responseText) { mainPanel.clear(); jsonItemData = getItemData(responseText); items = new ArrayList<MyItem>(); Item item = null; for (int i = 0; i < jsonItemData.length(); i++) { item = jsonItemData.get(i); items.add(new MyItem(item.getUserID(), item.getName(), item.getUsername(), item.getDivision(),item.getDepartment(), item.getItemID(),item.getDescription(), item.getJustification(),item.getOriginalDeparment(),
76
3233
3435
36
373839
332
333334
335336
337338
339340
341342
343344
item.getCreatedAt(),item.getUpdatedAt())); } CellTable<MyItem> table = new CellTable<MyItem>(); TextColumn<MyItem> nameColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.name; } }; TextColumn<MyItem> origDeptColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.origin_department; } }; TextColumn<MyItem> descColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.description; } }; TextColumn<MyItem> justColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.justification; } }; NumberCell numCell = new NumberCell(); Column<MyItem, Number> iidColumn = new Column<MyItem, Number>(numCell) { @Override public Integer getValue(MyItem item) { return item.iid; } }; DateCell createDateCell = new DateCell(); Column<MyItem,Date> createDateColumn = new Column<MyItem, Date>(createDateCell) { @Override public Date getValue(MyItem item) { return item.created_at; } };
77
345
346347
348349
350351
352353
354355
356357
358359
360361
362363
364365
366367
368369
370371
372373
374375
376377
378379
380381
382383
384385
386387
388389
390391
392393
394395
396397
398399
400401
402
DateCell updateDateCell = new DateCell(); Column<MyItem,Date> updateDateColumn = new Column<MyItem, Date>(updateDateCell) { @Override public Date getValue(MyItem item) { return item.updated_at; } }; ListDataProvider<MyItem> dataProvider = new ListDataProvider<MyItem>(); dataProvider.addDataDisplay(table); List<MyItem> list = dataProvider.getList(); for (MyItem itm : items) { list.add(itm); } final SingleSelectionModel<MyItem> selectionModel = new SingleSelectionModel<MyItem>(); table.setSelectionModel(selectionModel); selectionModel.addSelectionChangeHandler( new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent e) { MyItem selected = selectionModel.getSelectedObject(); if (selected != null) { Window.alert("" +selected.iid); } } }); makeSortableDateColumn(table,createDateColumn,list,"create"); makeSortableDateColumn(table,updateDateColumn,list,"update");// Lines 436 on are unmodified
The new lines are shown shaded. They are lines 35-36 and lines 420-433. Line 35 imports the SelectionChangeEvent class. This class is the type of event that is sent to the GWT application when a selection change occurs. Line 36 imports the SingleSelectionModel class. This class is implemented because we only want to allow selection of a single row of the CellTable.
Lines 420-433 allow the SingleSelectionModel to be applied to the CellTable. Lines 420-421 construct a SingleSelectionModel<MyItem> called selectionModel. The SelectionModel should have the same type as the row type of the CellTable. Note that this object is declared as final. This is because any objects that will be used inside an inner class need to be declared as final. Lines 424-433 define the Handler() object. This is the inner class that requires the SingleSelectionModel<MyItem> object to be declared as final. At this point, all this handler does is when there is a selection change caused by the mouse clicking on a different row, the row, which is of type MyItem, gets selected on line 428. Then, the item id of that row is shown in a Window.alert().
78
403
404405
406407
408409
410411
412413
414415
416417
418419
420421
422423
424425
426427
428429
430431
432433
434435
The next step would be to modify what is done in that handler so that we can display the field values of the item that needs to be edited. Here is the basic process that takes place:
1. The person running the program clicks on a table row to select it. This triggers the SelectionChangeEvent that is handled by the selection model's handler.
2. The selection model's handler responds by storing the item's id in the instance variable item_id.
3. If the user click on the “Edit item” button (that is added to the view of the items), a postRequest() is made to a PHP script that queries the database for the record from the item_view view that has that item id.
4. The PHP script returns that data, so that the postRequest()'s responseReceived() method can use the returned data to populate an input form that is similar to the input form for adding a new item.
5. When that input form is submitted, by clicking on a button, another postRequest() is made to a PHP script that updates the database.
6. Finally, the updated items for that user is redisplayed.
To carry this out, we need to do the following:
1. Add an instance variable of type int called item_id that can hold the item_id for the selected row. Create another instance variable of type Button called editItemButton with a label of “Edit item”. Create an instance variable of type Button called submitEditItemButton with a label of “Submit”.
2. In the showUserItems() method add editItemButton in a HorizontalPanel with the addItemButton.
3. In the selection model's handler, store the item id in item_id.
4. In the onClick() method, make the click handler of the editItemButton send a postRequest() to send the value of item_id.
5. Write a PHP script called “editItem.php” that gets the record from the item_view view that matches the item_id. The returned record is encoded as JSON to be read in the responseReceived() method of the calling postRequest().
6. In that responseReceived() that handles the record to be edited, call a method named “showEditItemPanel()” to display the item data. This panel will have include the submitEditItemButton that will be handled in the onClick() method by calling another postRequest() that sends the updated data to a PHP script called “updateItem.php”.
7. In the responseReceived() method of the postRequest() that updates the database, clear the main panel and call redrawUserItems() to redisplay the user's items.
79
In reviewing the steps above, it seems like some steps could be saved if the the other editors was saved to the items table. The SQL scripts would have to be adjusted to accommodate this, but this would make it so that a MyItem object would contain all the information needed to fill in the input form for editing existing data.
Adjustments to the SQL scripts
Here is the change to “createTables.sql”:
// Lines 1-21 are unmodifieddrop table if exists items cascade;create table items( id integer not null default nextval('item_id_seq'), description text, justification text, origin_department text, created_at timestamp default now(), updated_at timestamp default now(), others text, num_low integer default 0, num_med integer default 0, num_high integer default 0, primary key (id));// Lines 36 on are unmodified
Line 30 is the only new line. It is added so we can keep track of the others string. For example, if deptB and deptD are the additional editors, the value of others would be 'deptB,deptD'.
-- itemFuncs.sql
create or replace function addItemUser(_uid integer, _id integer) returns boolean as $func$ declare rec record; begin select into rec * from items_users where user_id=_uid and item_id=_id; if not found then insert into items_users (user_id, item_id) values (_uid, _id); else return 'f'; end if; return 't'; end; $func$ language 'plpgsql';
create or replace function addOtherEditors(_others text, _id integer)
80
22
2324
2526
2728
2930
3132
3334
35
1
234
56
78
910
1112
1314
1516
1718
1920
2122
23
returns boolean as $func$ declare v_id integer; my_array text[]; begin my_array := string_to_array(_others,','); for i in 1..array_length(my_array,1) loop v_id := cast (my_array[i] as integer); perform addItemUser(v_id,_id); end loop; return 't'; end; $func$ language 'plpgsql';
// Lines 1-39 unmodifiedcreate or replace function addItem(_uid integer, _desc text, _just text, _others text) returns boolean as $func$ declare rec record; rec2 record; div text; chair_id integer; it_id integer; begin select into rec * from users where id = _uid; if not found then return 'f'; else insert into items (description,justification, origin_department,others) values (_desc, _just, rec.department,_others); it_id := currval('item_id_seq'); div := rec.division; select into rec2 * from users where department=div; chair_id := rec2.id; perform addItemUser(_uid, it_id); if rec.id <> chair_id then perform addItemUser(chair_id, it_id); end if; if _others <> '' then perform addOtherEditors(_others,it_id); end if; return 't'; end if; end; $func$ language 'plpgsql';// Lines 74 on are unmodified
The only additions are on lines 56 and 57. These just add in the others field and the value for that field.
Here are the changes to “itemData.sql”:
81
24
2526
2728
2930
3132
3334
3536
3738
40
4142
4344
4546
4748
4950
5152
5354
5556
5758
5960
6162
6364
6566
6768
6970
7172
73
-- itemData.sqlinsert into items (description, justification, origin_department, others, created_at, updated_at) values ('item1', 'justification1','div1','deptA,deptC', timestamp '2012-01-01 9:30:22.1135',timestamp '2012-01-03');insert into items_users values(2,currval('item_id_seq'));insert into items_users values(3,currval('item_id_seq'));insert into items_users values(6,currval('item_id_seq'));
insert into items (description, justification, origin_department, others, created_at, updated_at) values ('item2', 'justification2','deptA','deptC', timestamp '2012-01-02 10:30:22.1135', timestamp '2012-01-04');insert into items_users values(3,currval('item_id_seq'));insert into items_users values(2,currval('item_id_seq'));insert into items_users values(6,currval('item_id_seq'));
insert into items (description, justification, origin_department, others, created_at, updated_at) values ('item3', 'justification3','deptA','', timestamp '2012-01-03 10:30:22.1135', timestamp '2012-01-04');insert into items_users values(3,currval('item_id_seq'));insert into items_users values(2,currval('item_id_seq'));
insert into items (description, justification, origin_department, others, created_at, updated_at) values ('item4', 'justification4','deptB','deptA', timestamp '2012-01-04 10:30:22.1135', timestamp '2012-01-05');insert into items_users values(4,currval('item_id_seq'));insert into items_users values(2,currval('item_id_seq'));insert into items_users values(3,currval('item_id_seq'));insert into items (description, justification, origin_department, others,created_at, updated_at) values ('item5', 'justification5','deptC','deptA', timestamp '2012-01-05 10:30:22.1135', timestamp '2012-01-06');insert into items_users values(6,currval('item_id_seq'));insert into items_users values(5,currval('item_id_seq'));insert into items_users values(3,currval('item_id_seq'));
insert into items (description, justification, origin_department, others,created_at, updated_at) values ('item6', 'justification6','deptC','deptB,deptD', timestamp '2012-01-05 10:30:22.1135', timestamp '2012-01-07');insert into items_users values(6,currval('item_id_seq'));insert into items_users values(5,currval('item_id_seq'));insert into items_users values(4,currval('item_id_seq'));insert into items_users values(7,currval('item_id_seq'));
insert into items (description, justification, origin_department, others,created_at, updated_at) values ('item7', 'justification7','deptD','deptA,deptC', timestamp '2012-01-05 10:30:22.1135', timestamp '2012-01-07');insert into items_users values(7,currval('item_id_seq'));insert into items_users values(5,currval('item_id_seq'));insert into items_users values(3,currval('item_id_seq'));insert into items_users values(6,currval('item_id_seq'));/*select addItem(2,'item1','justification1','3,6');
The modifications are shown shaded and occur on lines 3-4, line 8, lines 11-12, line 16, lines 19-20, lines 26-27, lines 33-34, lines 41-42, lines 50-51 and lines 58-59.
Line 3 adds in the others field and line 4 adds in the value for that field. Line 8 was modified to change the user id from 5 to 6, because 5 is the div2 chair.
Line 11 adds in the others field and line 12 adds in the value for that field. Line 16 was modified to change the user id from 5 to 6, because 5 is the div2 chair.
Line 19 adds in the others field and line 20 adds in the value for that field. Similar things are done on lines 26-27, lines 33-34, lines 41-42 and lines 50-51.
Lines 58-59 are modified to change the additional user id of 5 to 6, to reflect the changes on lines 8 and 16. Even though these lines are commented out, these changes were made to be consistent.
Here are the changes to “itemView.sql”:
-- itemView.sqlcreate or replace view item_view as select users.id as uid, users.name, users.username, users.division, users.department, items.id as iid, items.description, items.justification, items.origin_department, items.others, date_part('epoch',items.created_at) as created_at, date_part('epoch',items.updated_at) as updated_at from users inner join items_users on users.id=items_users.user_id inner join items on items_users.item_id=items.id; -- test queriesselect name,department,origin_department,description,others from item_view where uid=2;
Line 6 is added to include the others field. Line 13 is also modified to include the others field.
{} public final native int getUserID() /*-{ return this.uid; }-*/; public final native String getName() /*-{ return this.name; }-*/; public final native String getUsername() /*-{ return this.username; }-*/; public final native String getDivision() /*-{ return this.division; }-*/; public final native String getDepartment() /*-{ return this.department; }-*/; public final native int getItemID() /*-{ return this.iid; }-*/; public final native String getDescription() /*-{ return this.description; }-*/; public final native String getJustification() /*-{ return this.justification; }-*/; public final native String getOriginalDeparment() /*-{ return this.origin_department; }-*/; public final native String getOthers() /*-{ return this.others; }-*/; public final native String getCreatedAt() /*-{ return this.created_at; }-*/; public final native String getUpdatedAt() /*-{ return this.updated_at; }-*/;}
The lines 44-47 have been added to allow getting the others field.
Modifying Proj3Demo.java
There are a number of changes that needs to be made to “DemoProj3.java”. Instead of
84
7
89
1011
1213
1415
1617
1819
2021
2223
2425
2627
2829
3031
3233
3435
3637
3839
4041
4243
4445
4647
4849
5051
5253
5455
56
getting the item id when a table row is clicked on, we need to get the MyItem object that the row holds. Let's start with just a few changes so that we can compile and test to see if the application looks okay. Here is the modified “DemoProj3.java”:
// Lines 1-37 are unmodifiedpublic class DemoProj3 implements EntryPoint, ClickHandler{ VerticalPanel mainPanel = new VerticalPanel(); String baseURL = "http://localhost/demo-proj3/"; ArrayList<MyUser> users = new ArrayList<MyUser>(); ArrayList<MyItem> items = new ArrayList<MyItem>(); JsArray<User> jsonData; JsArray<Item> jsonItemData; VerticalPanel loginPanel = new VerticalPanel(); TextBox usernameBox; PasswordTextBox passBox; Button loginButton = new Button("Login"); int user_id; Button addItemButton = new Button("Add new item"); TextArea descBox; TextArea justBox; CheckBox deptA_cbox; CheckBox deptB_cbox; CheckBox deptC_cbox; CheckBox deptD_cbox; Button submitItemButton = new Button("Submit item"); MyItem selectedItem; Button editItemButton = new Button("Edit item"); Button submitEditItemButton = new Button("Submit"); private static class MyUser { private int id; private String name; private String username; private String division; private String department; public MyUser(int id, String name, String username, String division, String department) { this.id = id; this.name = name; this.username = username; this.division = division; this.department = department; } } private static class MyItem { private int uid; private String name; private String username; private String division; private String department;
{ mainPanel.clear(); jsonItemData = getItemData(responseText); items = new ArrayList<MyItem>(); Item item = null; for (int i = 0; i < jsonItemData.length(); i++) { item = jsonItemData.get(i); items.add(new MyItem(item.getUserID(), item.getName(), item.getUsername(), item.getDivision(),item.getDepartment(), item.getItemID(),item.getDescription(), item.getJustification(),item.getOriginalDeparment(), item.getOthers(), item.getCreatedAt(),item.getUpdatedAt())); } CellTable<MyItem> table = new CellTable<MyItem>(); TextColumn<MyItem> nameColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.name; } }; TextColumn<MyItem> origDeptColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.origin_department; } }; TextColumn<MyItem> descColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.description; } }; TextColumn<MyItem> justColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.justification; } }; NumberCell numCell = new NumberCell(); Column<MyItem, Number> iidColumn = new Column<MyItem, Number>(numCell) { @Override public Integer getValue(MyItem item)
87
342
343344
345346
347348
349350
351352
353354
355356
357358
359360
361362
363364
365366
367368
369370
371372
373374
375376
377378
379380
381382
383384
385386
387388
389390
391392
393394
395396
397398
399
{ return item.iid; } }; DateCell createDateCell = new DateCell(); Column<MyItem,Date> createDateColumn = new Column<MyItem, Date>(createDateCell) { @Override public Date getValue(MyItem item) { return item.created_at; } }; DateCell updateDateCell = new DateCell(); Column<MyItem,Date> updateDateColumn = new Column<MyItem, Date>(updateDateCell) { @Override public Date getValue(MyItem item) { return item.updated_at; } }; ListDataProvider<MyItem> dataProvider = new ListDataProvider<MyItem>(); dataProvider.addDataDisplay(table); List<MyItem> list = dataProvider.getList(); for (MyItem itm : items) { list.add(itm); } final SingleSelectionModel<MyItem> selectionModel = new SingleSelectionModel<MyItem>(); table.setSelectionModel(selectionModel); selectionModel.addSelectionChangeHandler( new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent e) { MyItem selected = selectionModel.getSelectedObject(); if (selected != null) { selectedItem = selected; } } }); makeSortableDateColumn(table,createDateColumn,list,"create"); makeSortableDateColumn(table,updateDateColumn,list,"update"); makeSortableTextColumn(table,origDeptColumn,list,"orig_dept"); makeSortableNumberColumn(table,iidColumn,list,"iid"); table.addColumn(nameColumn,"Name"); table.addColumn(iidColumn,"Item id"); table.addColumn(origDeptColumn,"Orig Dept"); table.addColumn(descColumn,"Description"); table.addColumn(justColumn,"Justification"); table.addColumn(createDateColumn,"Created"); table.addColumn(updateDateColumn,"Modified"); table.setRowCount(items.size(),true); table.setRowData(0,items); addItemButton.addClickHandler(this);
88
400
401402
403404
405406
407408
409410
411412
413414
415416
417418
419420
421422
423424
425426
427428
429430
431432
433434
435436
437438
439440
441442
443444
445446
447448
449450
451452
453454
455456
457
editItemButton.addClickHandler(this); HorizontalPanel buttonPanel = new HorizontalPanel(); buttonPanel.add(addItemButton); buttonPanel.add(editItemButton); mainPanel.add(buttonPanel); mainPanel.add(table); } // end showUserItems() private void redrawUserItems() {// Lines 467 on are unmodified
The modified lines are shown shaded. They are lines 60-62, line 92, line 100, line 112, lines 135-137, line 354, line 440 and lines 458-462.
Lines 60-62 add the new instance variables that are needed to edit an item. Line 60 adds a MyItem object called selectedItem. This will be used to hold the table row data when the mouse is used to select a row. Line 61 adds the button that will be clicked on to start the process of editing an item. Line 62 adds the button that will be used to submit data for updating an edited item.
Line 100 adds the others field to the MyItem constructor parameter list. Line 112 is used to set the value for others.
Lines 135-137 show the response to the ClickEvent generated when the editItemButton is hit. At this point, this does not do anything, but code will be added here to start the process to edit an item.
Line 354 is added to get the others value to help in the construction of the MyItem objects that populate the CellTable.
Line 440 stores the MyItem object for the row that was selected by the mouse.
Lines 458-462 are used to add the editItemButton right next to the additemButton whenever the user's items are displayed. Note that these buttons are placed in a HorizontalPanel so that they appear side-by-side.
Now, let's add the changes that will put up the item to be edited.
showAddItemPanel(); } else if (source == editItemButton) { mainPanel.clear(); showEditItemPanel(); } else if (source == submitItemButton) { String encData = URL.encode("description") + "=" +//// Lines 141-583 are unmodified// private void showAddItemPanel() { HTML heading = new HTML("<h2>Enter new item information:</h2>"); mainPanel.add(heading); Label descLabel = new Label("Description"); mainPanel.add(descLabel); descBox = new TextArea(); descBox.setSize("500", "100"); mainPanel.add(descBox); Label justLabel = new Label("Justification"); mainPanel.add(justLabel); justBox = new TextArea(); justBox.setSize("500","100"); mainPanel.add(justBox); HTML addUserHeading = new HTML("<h3>Check additional editors</h3>"); mainPanel.add(addUserHeading); deptA_cbox = new CheckBox("deptA"); deptB_cbox = new CheckBox("deptB"); deptC_cbox = new CheckBox("deptC"); deptD_cbox = new CheckBox("deptD"); HorizontalPanel checkboxRow = new HorizontalPanel(); checkboxRow.add(deptA_cbox); checkboxRow.add(deptB_cbox); checkboxRow.add(deptC_cbox); checkboxRow.add(deptD_cbox); mainPanel.add(checkboxRow); submitItemButton.addClickHandler(this); mainPanel.add(submitItemButton); } private void showEditItemPanel() { HTML heading = new HTML("<h2>Edit item information:</h2>"); mainPanel.add(heading); Label descLabel = new Label("Description"); mainPanel.add(descLabel); descBox = new TextArea(); descBox.setSize("500", "100"); descBox.setText(selectedItem.description); mainPanel.add(descBox); Label justLabel = new Label("Justification"); mainPanel.add(justLabel); justBox = new TextArea(); justBox.setSize("500","100"); justBox.setText(selectedItem.justification); mainPanel.add(justBox); HTML addUserHeading =
90
133
134135
136137
138139
140
584585
586587
588589
590591
592593
594595
596597
598599
600601
602603
604605
606607
608609
610611
612613
614615
616617
618619
620621
622623
624625
626627
628629
630
new HTML("<h3>Check additional editors</h3>"); mainPanel.add(addUserHeading); deptA_cbox = new CheckBox("deptA"); deptB_cbox = new CheckBox("deptB"); deptC_cbox = new CheckBox("deptC"); deptD_cbox = new CheckBox("deptD"); String[] otherEditors = selectedItem.others.split(","); for (int i = 0; i < otherEditors.length; i++) { if (otherEditors[i].equals("deptA")) { deptA_cbox.setValue(true); } else if (otherEditors[i].equals("deptB")) { deptB_cbox.setValue(true); } else if (otherEditors[i].equals("deptC")) { deptC_cbox.setValue(true); } else if (otherEditors[i].equals("deptD")) { deptD_cbox.setValue(true); } } HorizontalPanel checkboxRow = new HorizontalPanel(); checkboxRow.add(deptA_cbox); checkboxRow.add(deptB_cbox); checkboxRow.add(deptC_cbox); checkboxRow.add(deptD_cbox); mainPanel.add(checkboxRow); submitEditItemButton.addClickHandler(this); mainPanel.add(submitEditItemButton); }}
The new lines are shown shaded. They are lines 136-137 and lines 614-660. Lines 136 and 137 clear the main Panel and then call the showEditItemPanel() method. The showEditItemPanel() method is defined on lines 614-660.
The showEditItemPanel() method is similar to the showAddItemPanel() method. The differences are on line 616, line 622, line 628 and lines 637-651. Line 616 is just changed to show that we are editing an item, instead of adding an item. Lines 622, 628 and 637-651 get the values from the selected row and populate the input forms with the current values. Line 638-651 just define a for loop that iterates over the array of type String that is created by splitting the others value.
Displaying the “others” value in the CellTable
Since we now have the value of the others field, we should display this in the CellTable. This is relatively straightforward as this is just a String value.
// Lines 1-394 are unmodified NumberCell numCell = new NumberCell(); Column<MyItem, Number> iidColumn = new Column<MyItem, Number>(numCell) {
91
631
632633
634635
636637
638639
640641
642643
644645
646647
648649
650651
652653
654655
656657
658659
660661
395
396397
398
@Override public Integer getValue(MyItem item) { return item.iid; } }; TextColumn<MyItem> othersColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.others; } }; DateCell createDateCell = new DateCell(); Column<MyItem,Date> createDateColumn = new Column<MyItem, Date>(createDateCell) { @Override public Date getValue(MyItem item) { return item.created_at; } }; DateCell updateDateCell = new DateCell(); Column<MyItem,Date> updateDateColumn = new Column<MyItem, Date>(updateDateCell) { @Override public Date getValue(MyItem item) { return item.updated_at; } }; ListDataProvider<MyItem> dataProvider = new ListDataProvider<MyItem>(); dataProvider.addDataDisplay(table); List<MyItem> list = dataProvider.getList(); for (MyItem itm : items) { list.add(itm); } final SingleSelectionModel<MyItem> selectionModel = new SingleSelectionModel<MyItem>(); table.setSelectionModel(selectionModel); selectionModel.addSelectionChangeHandler( new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent e) { MyItem selected = selectionModel.getSelectedObject(); if (selected != null) { selectedItem = selected; } } }); makeSortableDateColumn(table,createDateColumn,list,"create"); makeSortableDateColumn(table,updateDateColumn,list,"update"); makeSortableTextColumn(table,origDeptColumn,list,"orig_dept");
92
399
400401
402403
404405
406407
408409
410411
412413
414415
416417
418419
420421
422423
424425
426427
428429
430431
432433
434435
436437
438439
440441
442443
444445
446447
448449
450451
452453
454455
456
makeSortableNumberColumn(table,iidColumn,list,"iid"); table.addColumn(nameColumn,"Name"); table.addColumn(iidColumn,"Item id"); table.addColumn(origDeptColumn,"Orig Dept"); table.addColumn(othersColumn,"Other editors"); table.addColumn(descColumn,"Description"); table.addColumn(justColumn,"Justification"); table.addColumn(createDateColumn,"Created"); table.addColumn(updateDateColumn,"Modified"); table.setRowCount(items.size(),true); table.setRowData(0,items); addItemButton.addClickHandler(this); editItemButton.addClickHandler(this); HorizontalPanel buttonPanel = new HorizontalPanel(); buttonPanel.add(addItemButton); buttonPanel.add(editItemButton); mainPanel.add(buttonPanel); mainPanel.add(table); } // end showUserItems()// Lines 476 on are unmodified
The new lines are shown shaded. They are lines 405-413 and line 461. Lines 405-413 just define a column that the others value can be inserted into. Line 461 adds the column to the CellTable.
Creating a PHP script to handle the item update
Let's create the file “updateItem.php” to handle the update of an item. This should be similar to what is done in the file “createItem.php”. The main difference is that we will do an update query versus an insert query. But, we also have to worry about updating the items_users table. The process for dealing with updating the items_users table has several steps:
1. Get the record from the users table that matches the submitter's uid. If the origin_department for the item does not match the submitter's department, no changes will be allowed to the items_users table. (Changes to the item's description and justification would be allowed for any of the allowed editors.) Get the item's old others value and overwrite the submitted otherDepts value.
2. If the submitter's department matches the item's origin_department, this is the only user allowed to make changes to the additional editors. Delete all records in the items_users table for that item id.
3. Update the items table with the submitted values.
4. Update the items_users table with the others value.
This could be a tricky task within the PHP script as it involves a number of queries. Therefore, it makes sense to write a plpgsql function called updateItem() that will carry out these processes. We will add this function to “itemFuncs.sql”:
93
457
458459
460461
462463
464465
466467
468469
470471
472473
474475
// Lines 1-104 are unmodifiedcreate or replace function updateItem(_uid integer, _iid integer, _desc text, _just text, _otherDepts text) returns boolean as $func$ declare itemview_rec record; submitter_rec record; oldEditors text; begin select into itemview_rec * from item_view where uid=_uid and iid=_iid; oldEditors = itemview_rec.others; select into submitter_rec * from users where id=_uid; update items set description=_desc, justification = _just where id=_iid; if itemview_rec.origin_department = submitter_rec.department then update items set others=_otherDepts where id=_iid; perform deleteOtherEditors(oldEditors,_iid); perform addOtherEditors(makeIDString(_otherDepts),_iid); end if; return 't'; end; $func$ language 'plpgsql'; create or replace function deleteOtherEditors(_oldEds text, _iid integer) returns boolean as $func$ declare idString text; myarray text[]; v_id integer; begin if _oldEds <> '' then idString := makeIDString(_oldEds); myarray := string_to_array(idString,','); for i in 1..array_length(myarray,1) loop v_id := cast (myarray[i] as integer); delete from items_users where user_id=v_id and item_id = _iid; end loop; end if; return 't'; end; $func$ language 'plpgsql';
Only the new lines that are added on the bottom are shown. These lines define the updateItem() function and the deleteOtherEditors() function.
Lines 109-112 declare local variables for the updateItem() function. On line 110 we
94
105106
107108
109110
111112
113114
115116
117118
119120
121122
123124
125126
127128
129130
131132
133134
135136
137138
139140
141142
143144
145146
147148
149150
151152
153
declare a record variable that will hold the record from the item_view view where the uid matches the passed user id and the iid matches the passed item id. On line 111 we declare a record variable that will hold the record from the users table where the id matches the passed user id. Line 112 will be used to store the value of the others field from the record selected from the item_view view.
On lines 114-115 we obtain the record from the item_view view that matches the passed user id and item id. This record allows us to get the origin_department field and others field values for the existing item. On line 116, we store the value from that others field in oldEditors. On lines 117-118 we obtain the record from the users table where the id matches the passed id. This is basically the record for the submitting user. Lines 117-118 update the description and justification fields. Those fields get updated for any user that is allowed to edit an item.
Only the original submitter is allowed to change the additional editors. So, the if statement defined on lines 121-127 will execute if the submitter is the original user that added the item. Otherwise, the function ends at that point. If the submitter is the original submitter, the others field is updated with the new string of departments that are allowed to edit the item. That is the purpose of the update query on lines 123-124. Line 125 calls the deleteOtherEditors() function to delete any records from the items_users table that were for additional editors.
The items_users table has several records for each inserted item. Suppose John Doe submits an item. John Doe is in deptA, so the division chair for deptA will also be allowed to edit the item. Suppose John Doe also adds deptB and deptD as editors. Based on the way the data was inserted in “itemData.sql”, the user id for Jane Doe, the div1 chair, is 2. John Doe's id is 3. The id for the deptB representative is 4, and the id for the deptD's representative is 7. The item id for John Doe's submission is 2. Therefore the items_users table will have entries like this:
submitter
div chair for submitter
additional editor
additional editor
When we update the additional editors we don't want to change the row for the original submitter and for the original submitter's div chair. We only want to delete the additional editors. Even if some of the original additional editors will remain, it is easier to delete all of
95
user_id item_id
3 2
2 2
4 2
7 2
the additional editors, and then add them back in again. So, on line 125 we get rid of all the old additional editors, and on line 126 we add the new additional editors.
The deleteOtherEditors() function defined on lines 133-152 is designed to get rid of all the records for the old additional editors from the items_users table. Lines 137-140 just declare the local variables needed for this function. On line 142, we check to see if the original others was an empty string. If it is, we don't delete any records. If the original others values is not an empty string, lines 143-149 are executed. On line 143, we use the makeIDString() function to convert all the department names into the user id numbers for the respective departments. On line 144 we split apart the string with the id numbers into an array of strings. This sets up the for loop defined on lines 144-149. That for loop will iterate over each of the user id numbers and delete any records from the items_users table that match the user id and the item id.
Now, lets create the PHP script “updateItem.php” so that it makes use of these functions.
In this version, lines 7-11 hardcode the POST data. Lines 13-17 just read in the data as though data were actually passed via POST. Lines 18-23 just prepare and call the updateItem() function. If this script is run from the command line using,php -f updateItem.php
the description and justification will be updated, but the others field will not be changed because the user with id number = 3 is not the original submitter. A query in the database
96
1
23
45
67
89
1011
1213
1415
1617
1819
2021
2223
2425
would show the results:
select description,justification,others,origin_department from items where id=1; description | justification | others | origin_department --------------------+----------------------+-------------+------------------- test description 1 | test justification 1 | deptA,deptC | div1(1 row)
As can be seen the description and justification are updated, but others is not. It is a good idea to do some further tests within the database. For example:
demo_proj3=> select description,justification,others,origin_department from items where id=1; description | justification | others | origin_department -------------+---------------+--------+------------------- desc item1 | just item1 | deptA | div1(1 row)
The first query calls the updateItem() function. Since the user with an id number = 2 is the original submitter, all of the fields are updated.
After testing to see that things work the way they should, you can just comment out lines 7-11 so that the POST data is no longer hardcoded. Then, we can go back to the GWT application and send the actual POST data. Here is “updateItem.php” with the hardcoded data commented out:
In running the GWT application, I discovered that a call to addOtherEditors() inside the addItem() function was not working. Here is the modified “itemFuncs.sql”:
// Lines 1-39 are unmodified create or replace function addItem(_uid integer, _desc text, _just text, _others text) returns boolean as $func$ declare rec record; rec2 record; div text; chair_id integer; it_id integer; begin select into rec * from users where id = _uid; if not found then return 'f'; else insert into items (description,justification, origin_department,others) values (_desc, _just, rec.department,_others); it_id := currval('item_id_seq'); div := rec.division; select into rec2 * from users where department=div; chair_id := rec2.id; perform addItemUser(_uid, it_id); if rec.id <> chair_id then perform addItemUser(chair_id, it_id); end if; if _others <> '' then perform addOtherEditors(makeIDString(_others),it_id); end if; return 't'; end if; end; $func$ language 'plpgsql';// Lines 74 on are unmodified
The change is to line 67. Instead of this,perform addOtherEditors(_others,it_id);
you have to call makeIDString(_others) like this,perform addOtherEditors(makeIDString(_others),it_id);
This was preventing items from being added. The other file that needs to be fixed is
Lines 16-20 are commented out because they are not needed. They will be removed later. On line 21, change :idstring to :others. On line 25, make the following changes. Instead of,':idstring' => $idString
replace with, ':others' => $otherDepts
Fixes to DemoProj3.java
The program was starting to do multiple additions of the same item when a new item was added, and the problem would get worse as additional items were added. This was caused by calling the addClickHandler() method inside of methods that get called multiple times to handle ClickEvents. The addClickHandler() method that designates the GWT application to handle the ClickEvents MUST be called inside the onModuleLoad() method. Otherwise, additional ClickHandlers are attached to the buttons each time the methods were called. Since I was calling the addClickHandler() method inside of showEditItemPanel(), showAddItemPanel(), showUserItems(), this caused multiple handlings of the same ClickEvent.
So, all the buttons should have the addClickHandler() method called inside the onModuleLoad() method.
Be sure to remove the addClickHandler() method from anywhere in the program that is not inside the onModuleLoad() or init(), since init() is only called inside onModuleLoad().
Now we can start making the additional changes to handle updating an item in “DemoProj3.java”:
// Lines 1-37 are unmodifiedpublic class DemoProj3 implements EntryPoint, ClickHandler{ VerticalPanel mainPanel = new VerticalPanel(); String baseURL = "http://localhost/demo-proj3/"; ArrayList<MyUser> users = new ArrayList<MyUser>(); ArrayList<MyItem> items = new ArrayList<MyItem>(); JsArray<User> jsonData; JsArray<Item> jsonItemData; VerticalPanel loginPanel = new VerticalPanel(); TextBox usernameBox; PasswordTextBox passBox; Button loginButton = new Button("Login"); int user_id; int item_id; Button addItemButton = new Button("Add new item"); TextArea descBox; TextArea justBox; CheckBox deptA_cbox; CheckBox deptB_cbox; CheckBox deptC_cbox; CheckBox deptD_cbox; Button submitItemButton = new Button("Submit item"); MyItem selectedItem; Button editItemButton = new Button("Edit item"); Button submitEditItemButton = new Button("Submit"); private static class MyUser {//// Lines 67–120 are unmodified//
100
194195
196197
198199
200201
202
3839
4041
4243
4445
4647
4849
5051
5253
5455
5657
5859
6061
6263
6465
66
public void onClick(ClickEvent e) { Object source = e.getSource(); if (source == loginButton) { String encData = URL.encode("username") + "=" + URL.encode(usernameBox.getText()) + "&"; encData += URL.encode("password") + "=" + URL.encode(passBox.getText()); String url = baseURL + "login.php"; postRequest(url,encData,"login"); } else if (source == addItemButton) { mainPanel.clear(); showAddItemPanel(); } else if (source == editItemButton) { mainPanel.clear(); showEditItemPanel(); } else if (source == submitItemButton) { String encData = getItemEncData(); String url = baseURL + "createItem.php"; postRequest(url, encData, "createItem"); } // else if source == addItemButton ends else if (source == submitEditItemButton) { String encData = getItemEncData(); encData += "&" + URL.encode("iid") + "=" + URL.encode("" + item_id); String url = baseURL + "updateItem.php"; postRequest(url, encData, "updateItem"); } } public void onModuleLoad() {//// Lines 155-186 are unmodified// private void postRequest(String url, String data, final String requestName) { final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,url); rb.setHeader("Content-type", "application/x-www-form-urlencoded"); try { rb.sendRequest(data, new RequestCallback() { public void onError(final Request request, final Throwable exception) { Window.alert(exception.getMessage()); } public void onResponseReceived(final Request request, final Response response) { if (requestName.equals("login")) { String resp = response.getText().trim(); user_id = Integer.parseInt(resp);
101
121
122123
124125
126127
128129
130131
132133
134135
136137
138139
140141
142143
144145
146147
148149
150151
152153
154
187188
189190
191192
193194
195196
197198
199200
201202
203204
205206
207
if (user_id < 1) { usernameBox.setText(""); passBox.setText(""); } else if (user_id == 1) { mainPanel.clear(); String url = baseURL + "showUsers.php"; getRequest(url,"getUsers"); } else { mainPanel.clear(); String encData = URL.encode("uid") + "=" + URL.encode("" + user_id); String url = baseURL + "showUserItems.php"; postRequest(url,encData,"getUserItems"); } } // ends if (requestName.equals("login")) else if (requestName.equals("getUserItems")) { showUserItems(response.getText()); } // ends else if requestName is "getUserItems" else if (requestName.equals("createItem")) { mainPanel.clear(); redrawUserItems(); } // ends elseif requestName is "createItem" else if (requestName.equals("updateItem")) { mainPanel.clear(); redrawUserItems(); } } // onResponseReceived() ends }); // new RequestCallback ends } catch (final Exception e) { Window.alert(e.getMessage()); } } // postRequest() ends private void showUsers(String responseText) {//// Lines 245-308 are unmodified// private void showUserItems(String responseText) { mainPanel.clear(); jsonItemData = getItemData(responseText); items = new ArrayList<MyItem>(); Item item = null; for (int i = 0; i < jsonItemData.length(); i++) { item = jsonItemData.get(i); items.add(new MyItem(item.getUserID(), item.getName(), item.getUsername(), item.getDivision(),item.getDepartment(), item.getItemID(),item.getDescription(), item.getJustification(),item.getOriginalDeparment(), item.getOthers(), item.getCreatedAt(),item.getUpdatedAt())); } CellTable<MyItem> table = new CellTable<MyItem>(); TextColumn<MyItem> nameColumn =
102
208
209210
211212
213214
215216
217218
219220
221222
223224
225226
227228
229230
231232
233234
235236
237238
239240
241242
243244
309
310311
312313
314315
316317
318319
320321
322323
324325
326
new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.name; } }; TextColumn<MyItem> origDeptColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.origin_department; } }; TextColumn<MyItem> descColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.description; } }; TextColumn<MyItem> justColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.justification; } }; NumberCell numCell = new NumberCell(); Column<MyItem, Number> iidColumn = new Column<MyItem, Number>(numCell) { @Override public Integer getValue(MyItem item) { return item.iid; } }; TextColumn<MyItem> othersColumn = new TextColumn<MyItem>() { @Override public String getValue(MyItem item) { return item.others; } }; DateCell createDateCell = new DateCell(); Column<MyItem,Date> createDateColumn = new Column<MyItem, Date>(createDateCell) { @Override
103
327
328329
330331
332333
334335
336337
338339
340341
342343
344345
346347
348349
350351
352353
354355
356357
358359
360361
362363
364365
366367
368369
370371
372373
374375
376377
378379
380381
382383
384
public Date getValue(MyItem item) { return item.created_at; } }; DateCell updateDateCell = new DateCell(); Column<MyItem,Date> updateDateColumn = new Column<MyItem, Date>(updateDateCell) { @Override public Date getValue(MyItem item) { return item.updated_at; } }; ListDataProvider<MyItem> dataProvider = new ListDataProvider<MyItem>(); dataProvider.addDataDisplay(table); List<MyItem> list = dataProvider.getList(); for (MyItem itm : items) { list.add(itm); } final SingleSelectionModel<MyItem> selectionModel = new SingleSelectionModel<MyItem>(); table.setSelectionModel(selectionModel); selectionModel.addSelectionChangeHandler( new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent e) { MyItem selected = selectionModel.getSelectedObject(); if (selected != null) { selectedItem = selected; item_id = selectedItem.iid; } } }); makeSortableDateColumn(table,createDateColumn,list,"create"); makeSortableDateColumn(table,updateDateColumn,list,"update"); makeSortableTextColumn(table,origDeptColumn,list,"orig_dept"); makeSortableNumberColumn(table,iidColumn,list,"iid"); table.addColumn(nameColumn,"Name"); table.addColumn(iidColumn,"Item id"); table.addColumn(origDeptColumn,"Orig Dept"); table.addColumn(othersColumn,"Other editors"); table.addColumn(descColumn,"Description"); table.addColumn(justColumn,"Justification"); table.addColumn(createDateColumn,"Created"); table.addColumn(updateDateColumn,"Modified"); table.setRowCount(items.size(),true); table.setRowData(0,items); HorizontalPanel buttonPanel = new HorizontalPanel(); buttonPanel.add(addItemButton); buttonPanel.add(editItemButton); mainPanel.add(buttonPanel); mainPanel.add(table); } // end showUserItems() private void redrawUserItems()