Sunday, December 29, 2013

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

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

Putting the Framework Together


This is a continuation of my previous blog. You will need to step through Part 1 and also my GWT 2.x and MySQL 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 framework in place where we can then start creating our web pages.


SearchEngines.html

To display our page we should first modify the HTML file in your project's "war" folder.  This HTML file will be the container for the entire site and all we need are nodes for GWT to attach the header, the main content, and a footer if we so desire.  Just a quick note that the div node is using a vertical CSS class from my blog on Fixing GWT's Layout Widgets that we will incorporate in Part 3 of this tutorial.

<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <link type="text/css" rel="stylesheet" href="SearchEngines.css">
    <title>Mom's Search Engine Rankings</title>
    <script type="text/javascript" language="javascript" src="searchengines/searchengines.nocache.js"></script>
  </head>
  <body style="background:#b0091c; line-height: 1.5;">
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
    <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>
      <div class="vertical mainpanel">
          <ul>
              <li id="header"/>
              <li id="content"/>
              <li id="footer"/>
          </ul>
      </div>
  </body>
</html>

Add Display Widgets

Let's now add the building blocks of our site with the HorizontalDivPanel and VerticalDivPanel from my previous blog, Fixing GWT's Layout Widgets that use CSS for your layout instead of the nasty GWT layout widgets that use HTML tables.  Place the HorizontalDivPanel.java and VerticalDivPanel.java in the client's widget package.

Project UI Constants

In the client package we will create a class that will hold all of the text that will appear in the UI.  No text that appears on the web pages should be hard coded, this rule will be very important when you go to translate your website into another language.  You might scoff and say, "But my website will never be localized!"  Even if the likelihood of your site going global is slim-to-none you should always be following best practices since cutting corners like this is a true sign of a shitty programmer.  Seriously I would look at your project and ask what podunk community college did you get your agriculture degree in before writing your code.

package com.example.searchengines.client;

import com.google.gwt.i18n.client.Constants;

/**
 * Project UI constants
 */
public interface SearchEnginesConstants extends Constants {
   
    @DefaultStringValue("About")
    String about();
    
    @DefaultStringValue("Add")
    String add();    
    
    @DefaultStringValue("Delete")
    String delete();
    
    @DefaultStringValue("Edit")
    String edit();
    
    @DefaultStringValue("Home")
    String home();
    
    @DefaultStringValue("Log In")
    String login();
    
    @DefaultStringValue("Log Out")
    String logout();

    @DefaultStringValue("Mom's Search Engine Ratings")
    String name();
    
    @DefaultStringValue("Register")
    String register();

    @DefaultStringValue("Hello")
    String welcome();
}

Project Configuration Properties

In the client package I like to create a configuration class that will hold some constants for the entire project.  Values like the version number, my email address, the server address (in case I want to do remote debugging), and some other crap we'll look at in another section of this tutorial. We are going to use a Java properties file to hold our values. I know the properties file are only supposed to be used for UI strings and localization, but seriously this is how every Java developer in the world handles their configuration settings and we will be no different. The one downside to this approach is with GWT. To load a properties file on the client-side you use the Constants interface, but the properties file (as far as I can tell) must be in the client package. This means if you want the server-side classes to use the same file you are shit out of luck. You'll have one property file in "src/main/resources" and duplicate file in "src/main/java/com.example.searchengines.client". It sucks I know. However if you use a build system like Maven you can make keeping these files in sync easier by automatically copying the file in the resources folder over the one in the client folder on every build. The benefit to this is to abstract some values from the code for when deploying to different environments such as production, QA, UAT, and your development servers. (Maybe I'll write a blog on that when I have time.)

First let's create a new file in the folder "src/main/resources" named "config.properties".  If you do not have that folder in your project you will want to create it, then right-click the "resources" folder and choose Build Path > Use as source folder. Here is the contents of "config.properties". If you haven't worked with Java property files before it is a simple text file with name value pairs.

mysql.host=localhost

mail.sender=me@fake.com

version=1.0.0
Now copy that file to the package "com.example.searchengines.client" with the name "Configuration.properties". Great, now we have our properties file in the 2 places where the client and the server can get to them. All that is left is creating a class to read the properties file so we can get to these values in our code. GWT has it's own mechanism (of course it does) to read properties files on the client-side. In your "client" package create a "Configuration" class with a properties for each of the values in the Configuration.properties file. You map each property to the properties file with the Key annotation.

Configuration.java
package com.example.searchengines.client;

import com.google.gwt.i18n.client.Constants;

public interface Configuration extends Constants {

    @Meaning("MySql hostname")
    @Key("mysql.host")
    @DefaultStringValue("localhost")
    String getMySqlHost();
    
    @Meaning("Email address of sender")
    @Key("mail.sender")
    String getMailSender();    
    
    @Meaning("Project version")
    @Key("version")
    String getVersion();
}

And finally create a class in the "server" package named "Configuration".

Configuration.java
package com.example.searchengines.server;

import java.util.ResourceBundle;

public class Configuration {

    private final String mysqlHost;
    private final String mailSender;
    private final String version;
    
    public Configuration() {
        ResourceBundle properties = ResourceBundle.getBundle("config");
        mysqlHost = properties.getString("mysql.host");
        mailSender = properties.getString("mail.sender");
        version = properties.getString("version");
    }

    public String getMysqlHost() {
        return mysqlHost;
    }

    public String getMailSender() {
        return mailSender;
    }

    public String getVersion() {
        return version;
    }
}


Presenter Interface

In the client's presenter package we will add a class called "Presenter.java" that will serve as our base class for all presenter classes.  It's pretty simple yet very important:

package com.rappa.lapizza.client.presenter;

import com.google.gwt.user.client.ui.HasWidgets;

public abstract interface Presenter {
    public abstract void go(final HasWidgets container);
}

View Base Class

In the client's view package we will add a class called "View.java" that will serve as our base class for all view classes and allow us easy access to the project's UI constant values.  Again pretty simple:

package com.example.searchengines.view;

import com.example.searchengines.client.SearchEnginesConstants;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;

/**
 * View base class
 */
public class View extends Composite {

    SearchEnginesConstants constants = GWT.create(SearchEnginesConstants.class);
    
    /**
     * View base class
     */
    public View() {
    }
    
        
    /**
     * Gets the widget
     */
    public Widget asWidget() {
        return this;
    }
}

MySQLConnection

Hey have you read through my blog GWT 2.x and MySQL? You will need to in order to get a MySQL connection to work. Trust me don't skip this step. Here is what our MySQLConnection class will look like for our project. Oh look there is the Configuration class we created earlier! Note that we are getting the MySQL host name from the value in the properties file.

package com.example.searchengines.server;

public class MySQLConnection extends RemoteServiceServlet implements DBConnection {

    private static final long serialVersionUID = 1L;
    private Configuration configuration = new Configuration();
    private String url = "jdbc:mysql://" + configuration.getMysqlHost() + ":3306/moms_search_engines";
    private String user = {your user who is not root}
    private String pass = {your password} 
    
    /**
     * Connection to mysql database
     */
    public MySQLConnection() {
    }

    /**
     * Gets the connection
     *
     * @return Connection state
     * @throws Exception
     */
    private Connection getConnection() throws Exception {
        Properties props = new Properties();
        props.setProperty("user", user);
        props.setProperty("password", pass);
        props.setProperty("zeroDateTimeBehavior", "convertToNull");
        Class.forName("com.mysql.jdbc.Driver").newInstance();
        Connection conn = DriverManager.getConnection(url, props);
        return conn;
    }
}

Data Factory

A data factory is a class that does nothing but contain pointers to other repositories.  We will create the data factory once in the client package, then pass it to every presenter so every page will have access to the same MySQL connection, the event bus to send notifications to individual presenters listening for events, and the currently logged in user.  It's not necessary that you use a data factory in your project, but I like to since it's easier passing just the factory to every presenter than n number of parameters that may increase as your project grows (causing rewriting a lot of code.) 

package com.example.searchengines.client;

import com.example.searchengines.service.DBConnectionAsync;
import com.example.searchengines.shared.User;
import com.google.gwt.event.shared.SimpleEventBus;

/**
 * Data factory to encapsulate a few things
 *
 */
public class DataFactory {
    private final DBConnectionAsync rpcService;
    private final SimpleEventBus eventBus;
    private final Configuration configuration;
    private User user;
    
    /**
     * Data factory to encapsulate a few things
     * @param rpcService Remote procedure call service
     * @param eventBus Event bus
     * @param configuration Project configuration
     * @param user User
     */
    public DataFactory(DBConnectionAsync rpcService, SimpleEventBus eventBus, Configuration configuration, User user) {
        this.rpcService = rpcService;
        this.eventBus = eventBus;
        this.configuration = configuration;
        this.user = user;                
    }

    /**
     * The MySQL connection
     * @return the rpcService
     */
    public DBConnectionAsync getRpcService() {
        return rpcService;
    }

    /**
     * The Event Bus
     * @return the eventBus
     */
    public SimpleEventBus getEventBus() {
        return eventBus;
    }

    /**
     * Project configuration
     * @return the configuration
     */
    public Configuration getConfiguration() {
        return configuration;
    }

    /**
     * Information on the currently logged in user
     * @param user the user to set
     */
    public void setUser(User user) {
        this.user = user;
    }

    /**
     * Information on the currently logged in user
     * @return the user
     */
    public User getUser() {
        return user;
    }
}

App Controller

Okay now we're really getting into the meat and potatoes of the MVP architecture!  The App Controller determines what page to display to the user by reading the URL and then instantiating the corresponding presenter and view classes as well as keeping a history of what URLs the user has accessed.  For example if the URL was "www.example.com/home" the App Controller sees the user is requesting the "home" page and will create a new home view and presenter object and attach it to the "content" element in our SearchEngines.html page.  My App Controller also contains a subclass that can read multiple tokens off of the URL.  For example we can call a presenter and pass some information that we want to store in the browser history, such as if the URL was "www.example.com/home?id=42" the home presenter knows the id value is 42.  If you still can't get your head around this go to www.amazon.com and start browsing the site and using filters like only show 4 star books under $100.  You might notice you are accessing the same page but some filter value is added as a token on the URL, and that is how you can call the same page but still have a browser's forward and back buttons still functional.

package com.example.searchengines.client;

import java.util.HashMap;
import com.example.searchengines.presenter.Presenter;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.ui.HasWidgets;


/**
 * The App Controller
 */
public class AppController implements Presenter, ValueChangeHandler<String> {
    private final DataFactory factory;
    private HasWidgets header;     //<li id="header">
    private HasWidgets contents;   //<li id="contents">

    /**
     * The App Controller
     * @param rpcService
     * @param eventBus
     */
    public AppController(DataFactory factory) {
        this.factory = factory;
        bind();
    }


    /**
     * Bind any listeners
     */
    private void bind() {
        History.addValueChangeHandler(this);

        //TODO 4: Listen for LogOutEvent 
    }
    

    /**
     * Attach view to header
     */
    public void setHeader(final HasWidgets header) {
        try { 
            this.header = header;
            //TODO 1: Add Header presenter and view
        }
        catch (Exception ex) {
     ex.printStackTrace();
        }
    }
    

    /**
     * Attaches view to container
     */
    public void go(final HasWidgets contents) {
        try {    
            //Set the contents
            this.contents = contents;

            //If there is no token then set the token to home otherwise fire a history onValueChange event
            if ("".equals(History.getToken())) {
                History.newItem("home");
            } else {
                History.fireCurrentHistoryState();
            }
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }       
    }
    

    /**
     * History and token management
     */
    public void onValueChange(ValueChangeEvent<String> event) {
        //Get the token from the URL
        Token token = new Token(event.getValue());
        
        if (token.getPage() != null) {
            Presenter presenter = null;            
                        
            //TODO 2: For every page you create set the presenter here
            
            //Load presenter
            if (presenter != null) {
                presenter.go(contents);            
            }
        }
    }
    
    
    
    /**
     * Url Token
     * @author Austin_Rappa
     *
     */
    class Token {
    
        HashMap<String, String> params = new HashMap<String, String>();
        private String page = null;
        private double id = 0d;
        
        
        public Token(String token) {
            this.page = this.parseHistoryToken(token);
        }
        
        
        /**
         * parse the historyToken
         * like domaint.tld#anchor?[var=1&var3=2&var3=3] 
         * @param historyToken anchor tag
         */
        private String parseHistoryToken(String historyToken) {
            
            if (historyToken == null) {
                return "";
            }
            
            //get parameters from history token
            if (historyToken.contains("?")) {
                HashMap<String, String> params = getHistoryTokenParameters(historyToken);

                //use the parameters
                setParams(params);

                //get just the history token / anchor tag , not with paramenters
                historyToken = getHistoryToken(historyToken);
            } 
        
            return historyToken;
        }
        
        /**
         * get historyToken parameters
         * like domaint.tld#anchor?[var=1&var3=2&var3=3]
         * @param historyToken anchor tag
         * @return hashmap of the parameters
         */
        private HashMap<String, String> getHistoryTokenParameters(String historyToken) {
        
            //skip if there is no question mark
            if (!historyToken.contains("?")) {
                return null;
            }
            
            // ? position
            int questionMarkIndex = historyToken.indexOf("?") + 1;
            
            //get the sub string of parameters var=1&var2=2&var3=3...
            String[] arStr = historyToken.substring(questionMarkIndex, historyToken.length()).split("&");
            HashMap<String, String>  params = new HashMap<String, String> ();
            for (int i = 0; i < arStr.length; i++) {
                String[] substr = arStr[i].split("=");
                if (substr.length == 2) {
                    params.put(substr[0], substr[1]);
                }
            }

            return params;
        }
        
        /**
         * get historyToken by itself
         * 
         * like domain.tld#[historyToken]?params=1
         *  
         * @param historyToken
         * @return
         */
        private String getHistoryToken(String historyToken) {
            
            //skip if there is no question mark
            if (!historyToken.contains("?")) {
                return "";
            }

            //get just the historyToken/anchor tag
            String[] arStr = historyToken.split("\\?");
            historyToken = arStr[0];
        
            return historyToken;
        }

        /**
         * are there params in historyToken
         * 
         * @return
         */
        @SuppressWarnings("unused")
        private boolean isParamsInHistoryToken() {
            String s = History.getToken();
            
            if (s.contains("?")) {
                return true;
            } else {
                return false;
            }
        }

        /**
         * use the parameters
         * @param params
         */
        private void setParams(HashMap<String, String>  params) {
            
            if (params == null) {
                return;
            }
                
            this.params = params;
            
            if (this.params.get("id") != null) {
                this.id = Double.parseDouble(this.params.get("id"));
            }            
        }
        

        /**
         * @return the page
         */
        public String getPage() {
            return this.page;
        }


        /**
         * @return the id
         */
        public double getId() {
            return this.id;
        }
    
        
        /**
         * @return the parameters
         */
        public HashMap<String, String> getParameters() {
            return this.params;
        }    
    }
}

Entry Point Class

We're in the home stretch!  In the entry point class SearchEngines.java is where we do all of our instantiation of our objects that we pass to the Data Factory such as the MySQL service and the event bus, then pass the Data Factory to the App Controller, give the App Controller what HTML nodes to attach itself to so we can insert our header and contents, and finally tell the App Controller to build the page.  By default that will be the home page unless a user has typed in a valid URL or bookmarked a page.

package com.example.searchengines.client;

import com.example.searchengines.service.DBConnection;
import com.example.searchengines.service.DBConnectionAsync;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.shared.SimpleEventBus;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.RootPanel;

/**
 * Entry point classes define <code>onModuleLoad()</code>.
 */
public class SearchEngines implements EntryPoint {

    /**
     * This is the entry point method.
     */
    public void onModuleLoad() {    
        //Connect to MySQL
        DBConnectionAsync rpcService = (DBConnectionAsync) GWT.create(DBConnection.class);
        ServiceDefTarget target = (ServiceDefTarget) rpcService;
        target.setServiceEntryPoint(GWT.getModuleBaseURL() + "MySQLConnection");
            
        //Event bus
        SimpleEventBus eventBus = new SimpleEventBus();

        //Configuration
        Configuration configuration = GWT.create(Configuration.class);
                    
        //Factory
        final DataFactory factory = new DataFactory(rpcService, eventBus, configuration, null);
        
        //AppController
        final AppController appViewer = new AppController(factory);
        appViewer.setHeader(RootPanel.get("header"));
        appViewer.go(RootPanel.get("content"));
    }
}

This seems like a good point to break.  Join us next time for another exiting adventure with Part 3: Finally Making the Page Content!

No comments:

Post a Comment