Sunday, December 29, 2013

A Real World Example of GWT 2.x, MySQL, and MVP: Part 3

Part 1: Getting Started
Part 2: Putting the Framework Together
Part 3: Finally Making the Page Content
Part 4: User Authentication 

Finally Making the Page Content


This is a continuation of my previous blog.

This tutorial is a how-to guide to help you build an actual, scalable, website with GWT and a MySQL back-end.  Right now if you try to Run or Debug your project it isn't going to do a lot as there is much to still set up.  Let's begin by starting from the bottom and continue to work our way up and by the end of this article we should have a site that we can launch from Eclipse with a home page, an about page, and getting data from our MySQL database.  W00t!

This page will follow the MVP architecture and is not intended to teach it to you.  In short you will have 2 classes for a single page, a presenter that handles all of the business logic, and a view that handles the layout.  If you want to know more than please first read an MVP tutorial like this.

I will not be using UiBinder feature since, GWT 2.5.1 the current version as of this article, is total crap when working with the MVP architecture.  Seriously it is if MVP and internationalization were an afterthought when Google added it to GWT.  I will create a parallel tutorial page for those using the UiBinder feature.

We will first review the header then work our way on to the different content pages.

Header Presenter

The header presenter is business logic for the header component of our page.  It contains an inner interface called "Display" that assists in communication with the header view.  The constructor has a parameter of the data factory and the header view object.

package com.example.searchengines.client.presenter;

import com.example.searchengines.client.DataFactory;
import com.example.searchengines.shared.User;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Widget;

/**
 * A presenter for the header
 */
public class HeaderPresenter implements Presenter {

    public interface Display {
        HasClickHandlers getHome();
        HasClickHandlers getAbout();
        HasClickHandlers getLogIn();
        HasClickHandlers getLogOut();
        HasClickHandlers getRegister();
        void setData();
        void setData(User user);
        void setSelected();
        Widget asWidget();
    }
    
    private final DataFactory factory;
    private final Display display;
    
    /**
     * A presenter for the header
     * @param factory The data factory
     * @param view The header view
     */
    public HeaderPresenter(DataFactory factory, Display view) {
        this.factory = factory;
        this.display = view;
    }
    
    
    /**
     * Bind events and actions
     */
    public void bind() {
        
        display.getHome().addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                History.newItem("home");
                display.setSelected();
            }            
        });
        
        display.getAbout().addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                History.newItem("about");
                display.setSelected();
            }
        });    
        
        display.getLogIn().addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                //TODO 10: Add login to history
            }
        });
        
        display.getLogOut().addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {    
                //TODO 5: Fire LogOutEvent           
            }
        });
        
        display.getRegister().addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                History.newItem("register");
                display.setSelected();
            }
        });
        
        //TODO 6: Listen for LogInEvents
    }

    
    /**
     * Go forth and build the page
     * @param contents Html content node
     */
    public void go(final HasWidgets contents) {
        bind();
        contents.clear();
        contents.add(display.asWidget());
        display.setSelected();
    }
}

Header View

The header view implements the Display interface of the header presenter, which helps to allow communication between the header and the presenter.  It has 2 setData() methods, one for when the site is accessed anonymously or when a user has logged in.

package com.example.searchengines.client.view;

import com.example.searchengines.client.presenter.HeaderPresenter;
import com.example.searchengines.client.widget.HorizontalDivPanel;
import com.example.searchengines.client.widget.VerticalDivPanel;
import com.example.searchengines.shared.User;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Label;

/**
 * Header page
 */
public class HeaderView extends View implements HeaderPresenter.Display {
            
    private final Button homeButton = new Button(constants.home());
    private final Button aboutButton = new Button(constants.about());
    private final Button loginButton = new Button(constants.login());
    private final Button logoutButton = new Button(constants.logout());
    private final Button registerButton = new Button(constants.register());
    private final HorizontalDivPanel rightPanel = new HorizontalDivPanel();
    

    /**
     * Header page
     */
    public HeaderView() {
        VerticalDivPanel mainPanel = new VerticalDivPanel();
        mainPanel.addStyleName("mainpanel");
        initWidget(mainPanel);            
        
        //Title
        Label titleLabel = new Label(constants.name());
        titleLabel.setStyleName("titletext");
        mainPanel.add(titleLabel);
        
        //Control Panel
        HorizontalDivPanel controlPanel = new HorizontalDivPanel();
        controlPanel.addStyleName("headerpanel");
        mainPanel.add(controlPanel);
            
            homeButton.setStyleName("headerbutton");
            controlPanel.add(homeButton);

            aboutButton.setStyleName("headerbutton");
            controlPanel.add(aboutButton);
            
            //Right panel
            controlPanel.add(rightPanel);
            controlPanel.setChildStyleName("alignright", controlPanel.getWidgetIndex(rightPanel));
            
        //Show default buttons
        setData();
    }
    
    
    /**
     * Set data for users not logged in
     */
    public void setData() {
        //Clear right panel
        rightPanel.clear();    
        
        //Login button
        registerButton.setStyleName("headerbutton");
        rightPanel.add(registerButton);
        
        Label seperatorLabel = new Label("|");
        seperatorLabel.setStyleName("headerpaneltext");
        rightPanel.add(seperatorLabel);
        
        //Login button
        loginButton.setStyleName("headerbutton");
        rightPanel.add(loginButton);
    }
        
    
    /**
     * Sets data for a user who is logged in
     */
    public void setData(User user) {
        //Clear right panel
        rightPanel.clear();
        
        //Hello user
        Label userLabel = new Label(constants.welcome() + " " + user.getFirstName());
        userLabel.setStyleName("headerpaneltext");
        rightPanel.add(userLabel);
        
        Label seperatorLabel = new Label("|");
        seperatorLabel.setStyleName("headerpaneltext");
        rightPanel.add(seperatorLabel);
            
        logoutButton.setStyleName("headerbutton");
        rightPanel.add(logoutButton);
    }
    
    
    /**
     * Sets a CSS style on the selected button
     */
    public void setSelected() {
        //Clear selected
        homeButton.removeStyleDependentName("selected");
        aboutButton.removeStyleDependentName("selected");
        loginButton.removeStyleDependentName("selected");
        logoutButton.removeStyleDependentName("selected");
        registerButton.removeStyleDependentName("selected");
        
        //Set selected
        String selected = History.getToken();        
        if (selected.equals("home")) {
            homeButton.setStyleDependentName("selected", true);
        }    
        else if (selected.equals("about")) {
            aboutButton.setStyleDependentName("selected", true);
        }
        else if (selected.equals("login")) {
            loginButton.setStyleDependentName("selected", true);
        }
        else if (selected.equals("logout")) {
            logoutButton.setStyleDependentName("selected", true);
        }
        else if (selected.equals("register")) {
            registerButton.setStyleDependentName("selected", true);
        }
    }
    

    /**
     * Home button
     */
    public HasClickHandlers getHome() {
        return this.homeButton;
    }


    /**
     * About button
     */
    public HasClickHandlers getAbout() {
        return this.aboutButton;
    }


    /**
     * Log in button
     */
    public HasClickHandlers getLogIn() {
        return this.loginButton;
    }


    /**
     * Log out button
     */
    public HasClickHandlers getLogOut() {
        return this.logoutButton;
    }


    /**
     * Registration button
     */
    public HasClickHandlers getRegister() {
        return registerButton;
    }
}


Adding the Header to the App Controller

Now that we have our header all defined and ready for prime time we just have to attach it to the HTML header node in the App Controller.  So open AppController.java and find TODO comment number 1:
//TODO 1: Add Header presenter and view
and replace it with an instantiation of the header presenter, passing in the data factory and a new header view:
Presenter presenter = new HeaderPresenter(factory, new HeaderView());
presenter.go(header);

So your final setHeader() method should look like this:
public void setHeader(final HasWidgets header) {
    try {    
        this.header = header;
        Presenter presenter = new HeaderPresenter(factory, new HeaderView());
        presenter.go(header);
    }
    catch (Exception ex) {
        ex.printStackTrace();
    }
}

Home Presenter

package com.example.searchengines.client.presenter;

import java.util.List;
import com.example.searchengines.client.DataFactory;
import com.example.searchengines.shared.SearchEngine;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.SingleSelectionModel;

/**
 * A presenter for the home page
 */
public class HomePresenter implements Presenter {

    public interface Display {
        HasClickHandlers getAdd();        
        SingleSelectionModel<SearchEngine> getSelectionModel();
        void setData(List<SearchEngine> rowData);
        void showError(String message);
        Widget asWidget();
    }

    private final DataFactory factory;
    private final Display display;

    
    /**
     * A presenter for the home page
     * @param factory
     * @param view
     */
    public HomePresenter(DataFactory factory, Display view) {
        this.factory = factory;
        this.display = view;
    }

    
    /**
     * Bind events and actions
     */
    public void bind() {
    }
     
    
    /**
     * Go forth and build the page
     */
    public void go(final HasWidgets container) {
        bind();
        container.clear();
        container.add(display.asWidget());
        this.fetchData();
    }
    
    
    /**
     * Fetch page information
     */
    private void fetchData() {
        try {                    
            //TODO 3: Let's use MySQL!
        } catch (Exception e) {
            e.getStackTrace();
        }
    }
}

Home View

The home view will use my custom StarRatingStarRatingCell and StarRatingColumn widgets from my previous blogs where you can add them to your client's "widget" package.

package com.example.searchengines.client.view;

import java.util.List;
import com.example.searchengines.client.presenter.HomePresenter;
import com.example.searchengines.client.widget.HorizontalDivPanel;
import com.example.searchengines.client.widget.StarRating;
import com.example.searchengines.client.widget.StarRatingColumn;
import com.example.searchengines.client.widget.VerticalDivPanel;
import com.example.searchengines.shared.SearchEngine;
import com.google.gwt.cell.client.ClickableTextCell;
import com.google.gwt.cell.client.FieldUpdater;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.cellview.client.TextColumn;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.view.client.DefaultSelectionEventManager;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SingleSelectionModel;

/**
 * Home page
 */
public class HomeView extends View implements HomePresenter.Display {    
    
    private Label errorLabel = new Label();
    private Button addButton = new Button(constants.add());
    private CellTable<SearchEngine> cellTable;
    private SingleSelectionModel<SearchEngine> selectionModel;
    
    /**
     * Home page
     */
    public HomeView() {
        
        //Main panel
        VerticalDivPanel mainPanel = new VerticalDivPanel();
        mainPanel.addStyleName("mainpanel");
        mainPanel.addStyleName("homepage");
        initWidget(mainPanel);

        errorLabel.setStyleName("errorlabel");
        errorLabel.setVisible(false);
        mainPanel.add(errorLabel);
        
        //TODO 7: Add new search engine button
        
        //Welcome
        VerticalDivPanel dataPanel = new VerticalDivPanel();
        dataPanel.addStyleName("datapanel");
        mainPanel.add(dataPanel);

            //Table key provider for sorting
            ProvidesKey<SearchEngine> keyProvider = new ProvidesKey<SearchEngine>() {
                public Object getKey(SearchEngine item) {                  
                    return (item == null) ? null : item.getId();       
                }     
            };                                                 
            
            //Data table
            cellTable = new CellTable<SearchEngine>(keyProvider);
            cellTable.setWidth("100%");    
            mainPanel.add(cellTable);
            
            //Add a selection model to handle user selection.     
            selectionModel = new SingleSelectionModel<SearchEngine>(keyProvider);     
            cellTable.setSelectionModel(selectionModel, DefaultSelectionEventManager.<SearchEngine>createDefaultManager());             
        
                //Create name column.
                Column<SearchEngine, String> nameColumn = new Column<SearchEngine, String>(new ClickableTextCell()) {
                    @Override
                    public String getValue(SearchEngine searchEngine) {
                        return searchEngine.getName();
                    }     
                };
                nameColumn.setFieldUpdater(new FieldUpdater<SearchEngine, String>() {   
                    public void update(int index, SearchEngine searchEngine, String value) {     
                        Window.open(searchEngine.getUrl(),"_blank","");
                    } 
                }); 
                nameColumn.setCellStyleNames("hyperlink");
                cellTable.addColumn(nameColumn, constants.headername());    
                
                //Create url column.
                TextColumn<SearchEngine> urlColumn = new TextColumn<SearchEngine>() {
                    @Override
                    public String getValue(SearchEngine searchEngine) {
                        return searchEngine.getUrl();
                    }     
                }; 
                cellTable.addColumn(urlColumn, constants.headerurl());
                
                //Category column.
                TextColumn<SearchEngine> categoryColumn = new TextColumn<SearchEngine>() {
                    @Override
                    public String getValue(SearchEngine searchEngine) {
                        return searchEngine.getCategory().toString();
                    }     
                }; 
                cellTable.addColumn(categoryColumn, constants.headercategory());
                        
                //Rating column
                StarRatingColumn<SearchEngine> ratingColumn = new StarRatingColumn<SearchEngine>() {
                    @Override    
                    public StarRating getValue(SearchEngine searchEngine) {
                        return new StarRating(searchEngine.getRating(), 10, true);                    
                    }                                  
                };
                cellTable.addColumn(ratingColumn, constants.headerrating());    
                
                //TODO 8: Admin controls
    }
    
    
    /**
     * Add button
     */
    public HasClickHandlers getAdd() {
        return this.addButton;
    }

    
    /**
     * The table selection model
     */
    public SingleSelectionModel<SearchEngine> getSelectionModel() {
        return selectionModel;
    }


    /**
     * Set table data
     * @param data The data to set
     */
    public void setData(List<SearchEngine> data) {
        cellTable.setRowData(data);
    }


    /**
     * Show error message
     */
    public void showError(String message) {
        errorLabel.setText(message);
        errorLabel.setVisible(true);
    }
}

Adding the Home Page to the App Controller

Now that we have our home component all defined we just have to attach it to the HTML contents node.  So open AppController.java and find TODO comment number 2:
//TODO 2: For every page you create set the presenter here
and replace it with an instantiation of the header presenter, passing in the data factory and a new header view:
if (token.getPage().equals("home")) {
    presenter = new HomePresenter(factory, new HomeView());
} 

So your onValueChange() method should look like this:
public void onValueChange(ValueChangeEvent<String> event) {
    Token token = new Token(event.getValue());
        
    if (token.getPage() != null) {
        Presenter presenter = null;            
            
        // For every page you create set the presenter here
        if (token.getPage().equals("home"))    {
            presenter = new HomePresenter(factory, new HomeView());
        }        
            
        //Load presenter
        if (presenter != null) {
            presenter.go(contents);            
        }
    }
}


About Presenter

We will create an about page, which will just be a page with a little text blurb just so we have another page to click around to.
package com.example.searchengines.client.presenter;

import com.example.searchengines.client.DataFactory;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Widget;

/**
 * A presenter for the about page
 */
public class AboutPresenter implements Presenter {

    public interface Display {
        Widget asWidget();
    }

    @SuppressWarnings("unused")
    private final DataFactory factory;
    private final Display display;
    
    /**
     * A presenter for the about page
     * @param factory
     * @param view
     */
    public AboutPresenter(DataFactory factory, Display view) {
        this.factory = factory;
        this.display = view;
    }
    
    /**
     * Bind events and actions
     */
    public void bind() {
    }
      
    /**
     * Go forth and build the page
     */
    public void go(final HasWidgets container) {
        bind();
        container.clear();
        container.add(display.asWidget());
    }
}

About View

Before we can create our about view we have to first add a new string constant to SearchEngineConstants.java in our "client" package.
@DefaultStringValue("This is a website for my mom to help her find things on the web.")
String abouttext();

Now we can use it in our view.
package com.example.searchengines.client.view;

import com.example.searchengines.client.presenter.AboutPresenter;
import com.example.searchengines.client.widget.VerticalDivPanel;
import com.google.gwt.user.client.ui.Label;

/**
 * About page
 */
public class AboutView extends View implements AboutPresenter.Display {    

    /**
     * About page
     */
    public AboutView() {
        
        //Main panel
        VerticalDivPanel mainPanel = new VerticalDivPanel();
        mainPanel.addStyleName("mainpanel");
        mainPanel.addStyleName("aboutpage");
        initWidget(mainPanel);
        
        Label aboutLabel = new Label(constants.abouttext());
        mainPanel.add(aboutLabel);        
    }    
}

Adding the About Page to the App Controller

In the HeaderPresenter.java class you will see we have already added a click handler for the about button to set the history to "about".  This will change the url in the browser from "www.mysite.com/home" to "www.mysite.com/about" and if you remember the App Controller is listening for changes to the history where we can then set what page is called depending on the current history value.  All we have to do is go back to AppController.java to the TODO comment number 2 and add an else if statement to set the presenter to an AboutPresenter.

// For every page you create set the presenter here
if (token.getPage().equals("home")) {
    presenter = new HomePresenter(factory, new HomeView());
}        
else if (token.getPage().equals("about")) {
    presenter = new AboutPresenter(factory, new AboutView());
}

Getting Data From MySQL

Now we need to add a a way for us to retrieve a list of active search engines to fill our table on the home page.  Add the following method to your MySQLConnection.java class in the "server" package:

/**
* Gets a list of search engines
* @return An ArrayList of SearchEngine
* @throws Exception
*/
public List<SearchEngine> getSearchEngines() throws Exception {
    List<SearchEngine> returnObjects = new ArrayList<SearchEngine>();
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet result = null;

    try {
        conn = getConnection();
        pstmt = conn.prepareStatement("SELECT * FROM search_engines WHERE is_active = TRUE ORDER BY rating DESC");
        result = pstmt.executeQuery();
        while (result.next()) {
            SearchEngine searchEngine = new SearchEngine();
            searchEngine.setId(result.getDouble("id"));
            searchEngine.setName(result.getString("name"));
            searchEngine.setUrl(result.getString("url"));
            searchEngine.setCategory(Category.fromInteger(result.getInt("category_id")));
            searchEngine.setRating(result.getInt("rating"));
            searchEngine.setActive(result.getBoolean("is_active"));
            returnObjects.add(searchEngine);
        }
    } catch (SQLException sqle) {
        logger.error("SQL error in getSearchEngines(): " + sqle.getMessage() + "\n" + sqle.getStackTrace().toString());
    } finally {
        result.close();
        pstmt.close();
        conn.close();
    }

    return returnObjects;
}

Add this method to your DBConnection.java class in the client's "services" package:

/**
 * Gets a list of search engines
 * @return An ArrayList of BestRating
 * @throws Exception
 */
public List<SearchEngine> getSearchEngines() throws Exception;

And finally add this method to your DBConnectionAsync.java class in the client's "services" package:

/**
 * Gets a list of search engines
 * @param callback Async return of the SearchEngine List
 * @return An ArrayList of SearchEngine
 * @throws Exception
 */
public void getSearchEngines(AsyncCallback<List<SearchEngine>> callback) throws Exception;

Now that we have a way to query data from our MySQL database and transfer it to our client let's go ahead an hook it up.  Open HomePresenter.java in your client's "presenter" package and find the TODO comment number 3:
//TODO 3: Let's use MySQL!
and replace it with a call to the method we just created through the data factory.
try {                    
    factory.getRpcService().getSearchEngines(new AsyncCallback<List<SearchEngine>>() {
        public void onFailure(Throwable caught) {
            display.showError("Failure getting the search engines: " + caught.getMessage());
        }

        public void onSuccess(List<SearchEngine> result) {
            display.setData(result);
        }
    });
} catch (Exception e) {
    display.showError("Error getting the search engines: " + e.getMessage());
}

So your fetchData() method should look like this:
private void fetchData() {
        try {                    
            factory.getRpcService().getSearchEngines(new AsyncCallback<List<SearchEngine>>() {
                public void onFailure(Throwable caught) {
                    display.showError("Failure getting the search engines: " + caught.getMessage());
                }

                public void onSuccess(List<SearchEngine> result) {
                    display.setData(result);
                }
            });
        } catch (Exception e) {
            display.showError("Error getting the search engines: " + e.getMessage());
        }
    }


Stylin'

Look, I suck at CSS so don't judge me alright.  Place this in your SearchEngines.css file in your project's "war" folder:
/** --- Html --- */
html {height:100%;}
body {height:100%;}
div {display:block;}
table {border-collapse:collapse; border-spacing:0;}

/** --- DivPanels --- */
.vertical {vertical-align: top;    margin: 0px; padding: 0px; width: 100%;}
.vertical > ul {list-style: none; margin: 0px; padding: 0px; width: 100%;}
.vertical > ul > li {display: block; text-decoration: none; margin: 0px; padding:0px 0px; overflow:hidden;}
.horizontal {padding: 0px; margin: 0px;    width: 100%;}
.horizontal > ul {float: left; list-style: none; padding: 0px; margin: 0px; width: 100%;}
.horizontal > ul > li {float: left;    display: block;    text-decoration: none; margin: 0px;    padding: 0px;}

/** --- Main Panels --- */
.alignright {float:right!important;}
.mainpanel {background:#fff9fe; min-width:780px; max-width:1260px; min-height:100%; margin:0px auto; padding:10px}
.errortext {color:red;}

/** -- Header -- */
.headerpanel {height:30px; background:#eeeeee; border:2px solid #cccccc; width:1240px;}
.headerbutton {border:none; margin:0px 2px; padding:6px 15px; color:#black; cursor:pointer; background:transparent;}
.headerbutton:hover {background:#dddddd;}
.headerbutton-selected {background:#fffaa5;}
.headerbutton-selected:hover {background:#fffaa5;}
.headerpaneltext {padding:6px 3px; color:black; background:transparent;}
.titletext {font-size:200%; margin:25px 0px 5px 0px;}

Debug Time!

Holy crap! Quick, right click your project and choose Debug As > Web Application, then in the Development Mode tab right click http://127.0.0.1:8888/SearchEngines.html?gwt.codesvr=127.0.0.1:9997  and choose Open or Open With and then whatever Chrome browser Chrome you Chrome prefer.  Hopefully you will see a page that looks something like this:


Hooray! I'm not an idiot! (At least when it comes to GWT.)  Don't worry if the page seems slow to load, that is just the debugger and will change once you place it on an actual web server.  I'm glad you took the time and stepped through each of my tutorials... or you could have cheated and downloaded the source here: Download Source

Now let's add some authentication!

2 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Thanks for tutorial. I'm getting something like this when loading page:
    Failure getting the search engines: 404 Not Found < html> < head> < meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/> < title>Error 404 Not Found< /title> < /head> < body>< h2>HTTP ERROR 404< /h2> < p>Problem accessing /searchengines/MySQLConnection. Reason: < pre> Not Found< /pre>< /p>< hr />< i>< small>Powered by Jetty://< /small>< /i>< br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < br/> < /body> < /html>

    ReplyDelete