Document Information

Last modified:
2011/07/08 16:40 by c.boulanger

Tutorial: How to create a client-server application with qooxdoo, qcl and QxTransformer

This tutorial is currently out of date. The examples do not reflect the current state of the API and cannot be used with more recent versions of qcl, qxtransformer, and qooxdoo. It is here only to show you what could be re-implemented if I only had time for it:-(
The following tutorial provides a short introduction into the complex matter of writing a full-fledged client-server application. The building blocks of this project are qooxdoo, the qcl class library, and QxTransformer toolkit. If you are looking for an introduction in QxTransformer, which can be used without the qcl library, look here. More information on qcl can be found here.

This tutorial will show you how to use the qcl library to create a fully functional simple application which contains all the necessary building blocks for a web application such as authentication, access control, configuration and a backend for data storage. I use the "access" application, which can be found in the qooxdoo-contrib repository as an example.

For this, I will also use the xml-to-javascript converter QxTransformer, which is not neccessary to use the features provided by qcl, but which makes writing applications alot faster. You will soon see why.

Prerequisites

Before starting to work on the application, you need the following things:

  • qooxdoo-sdk: qcl 0.5 (unreleased) supports qooxdoo 1.0.
  • There dependencies (qcl library, the RpcPhp server and QxTransformer) are installed automatically from qooxdoo-contrib. QxTransformer has a dependency on the python LXML module, see here how to setup the environment to run QxTransformer.
  • PHP5: PHP4 is no longer supported.
  • MySql database. In theory, other databases should work as well (since the database interface is abstracted by PDO), but there is still a fair amount of raw SQL in the code that might not be compatible. It should be trivial to write adapters that cover the differences.

First steps

Once you have made you sure have all the prerequisites, it is time to create a sample application. Let's call the application namespace "access", since I will use an already existing application from the qcl svn repository. A public demo installation of this application is available (currently unfunctional). The version that we will build in this tutorial is a much simplified version of this app and the source differs from the code in qooxdoo-contrib. However, it might be useful for you to consult the source code in SVN to compare to the code of the tutorial or the code of your own project.

Create qooxdoo skeleton application

Follow the Tutorial on how to create a new application using the create-application.py script. Make sure to name your application "access" (–name=access) to keep in sync with the examples of this tutorial.

Customize config.json

The next step is to configure the config.json file for use with QxTransformer and the other dependencies. You can view the full config.json file here.

1. Add constants and libraries

In the 'let' section, you need to need to add the path to the python executable (dependent on your platform and setup) and then add the two lines with the "CONTRIB_PATH" and "QXTRANSFORMER_PATH" constants.

  "let" :
  {
   ...
    
    // QxTransformer
    "PYTHON"       : "/path/to/python2.5",
    "CONTRIB_PATH" : "contrib",
    "QXTRANSFORMER_PATH": "${CONTRIB_PATH}/QxTransformer/trunk/tool"
  },

The default used here for the "CONTRIB_PATH" constant is a directory "contrib" in the application root directory. The generator will download the contributions to this directory. If you have several projects that should share the downloaded contributions, use an absolute path to a directory that is accessible to your webserver.

In the 'jobs/libraries/library' section, add the path to the manifest files of the required libraries.

"jobs""libraries""library"// QxTransformer
"manifest""contrib://QxTransformer/trunk/Manifest.json"// qcl-js
"manifest""contrib://qcl/trunk/Manifest.json"// qcl-php
"manifest""contrib://qcl-php/trunk/Manifest.json"// PHP JSON-RPC server
"manifest""contrib://RpcPhp/trunk/Manifest.json"// Dialogs 
"manifest""contrib://Dialog/trunk/Manifest.json"// download contrib code to location that is accessible to 
// web server in source version
"source-script""cache""downloads""${CONTRIB_PATH}""common""settings""jsonrpc.backend""RpcPhp"

2. Download contributions

Now execute ./generate.py source to download the libraries. This might take quite a while (up to 10 minutes). After this first download, the generator will have to download the libraries only if they are updated in the repository.

3. Add "transform" job

You need to edit config.json again: In the 'include' section, add the entry for the QxTransformer job.

  "include" :
  [
    {
      "path" : "${QOOXDOO_PATH}/tool/data/config/application.json"
    },
    {
      "path" : "${QXTRANSFORMER_PATH}/bin/qxtransformer.json"
    }
  ],

Now, in the 'export' section, add a "transform" entry.

 "export" :
  [
    ...
    "translation",
    
   // Qxtransformer
    "transform"
  ],

Creating the frontend

Now you are ready to create an xml-based application frontend. The declarative syntax we will use is QXML, the syntax understood by QxTransformer. You could also use plain old javascript to do that. However, using QXML speeds up the design of user interfaces in qooxdoo considerably, so we will use it for this tutorial.

The main application file

First, we need to create the main application file. In the source/class/access folder, create the file Application.xml:

source/class/access/Application.xml

<?xml version="1.0" encoding="utf-8"?>
<qx:application 
  xmlns:qx="http://www.qxtransformer.org/qooxdoo/0.8"
  xmlns:qxt="http://www.qxtransformer.org/extension/0.4"
  xmlns:qcl="http://www.qooxdoo.org/contrib/qcl/trunk"
  xmlns:components="access.components.*" 
  className="access.Application"
  author="Your Name"
  extend="access.Main">
 
  <!-- main layout -->
 
  <qcl:loginDialog 
    image="access/qooxdoo-logo.gif"
    widgetId="loginDialog"
    callback="{js}this.checkLogin"
    message="Please log in."
    text="qcl demo application"
    />
 
  <qx:composite qxt:edge="0">
    <qx:vbox>
 
      <!-- toolbar -->
      <components:toolBar/>
 
      <qx:composite margin="30">
        <qx:vbox spacing="10">
 
        <!-- application body -->
        <components:body/>
 
        <!-- source code buttons -->
        <components:footer/>      
 
        </qx:vbox>
      </qx:composite>
 
    </qx:vbox>
  </qx:composite>
</qx:application>

What this does is to tell QxTransformer to create a class "access.Application" in a file Application.js that extends the class access.Main (which we will create next). This class will draw a GUI consisting of three components, the toolbar, and the application body, which will be contained in classes in the "access.components.*" namespace (See more on the QxTransformer components model), and a footer widget.

You could also put the whole application in this one file, without using components. However, it is good practice to modularize your application into the constituent parts. This makes it much easier to maintain the application and to read the source code. Note the namespaces used in the xml document.

Application logic

Second, we need the basic business logic for the application, which will be contained in the access.Main class. The class generated from Application.xml extends from this class (see the extend="access.Main" attribute of the <qx:application> tag).

source/class/access/Main.js

/* ************************************************************************
#asset(access/*)
#asset(qx/*)
#require(qcl.application.*)
************************************************************************ */
 
/**
 * Class definition
 */
qx.Class.define("access.Main",
{
  extend : qx.application.Standalone,
  include : [ qcl.application.MAppManagerProvider ],
 
  properties :
  {
    /**
     * The backend server used
     * @type 
     */
    server :
    {
      check    : "String",
      nullable : true,
      apply    : "_applyServer",
      event    : "changeServer"
    }
  },
 
  events:
  {
    "changeServer" : "qx.event.type.Data"
  },
 
  members :
  {
    /**
     * Initialize the application
     *
     * @return {void} 
     */
    main : function()
    {
      this.base(arguments);
 
      /**
       * initialize the managers
       */
      this.initializeManagers();
 
      /*
       * logging
       */
      if (qx.core.Variant.isSet("qx.debug", "on")) {
        qx.log.appender.Native;
      }
 
      dialog.Dialog.init(); // FIXME
 
      this.info("Starting Application...");
 
      /*
       * Setup authentication and configuration without
       * setting the service methods
       */
      this.getAccessManager().init();
      this.getConfigManager().init();
 
      /*
       * setup server state, this will configure
       * the service methods and start auth/config
       */
      if ( ! this.getStateManager().getState("server") ) {
        this.setServer("rpcphp"); // this updates the state,too
      } else {
        this.getStateManager().updateState("server"); // this updates the property, too
      }
 
      /*
       * Greet the visitor!
       */
 
      dialog.alert("Welcome to the Access Demo Application!");
    },
 
 
    /**
     * Callback function that takes the username, password and
     * another callback function with its context as parameters. 
     * The passed function is called with a boolean value 
     * (true=authenticated, false=authentication failed) and an 
     * optional string value which can contain an error message :
     * callback.call( context, {Boolean} result, {String} message);
     *
     * @param username {String} TODOC
     * @param password {String} TODOC
     * @param callback {Function} The callback function
     * @param context {Object} The execution context of the callback
     * @return {void} 
     */
    checkLogin : function(username, password, callback, context)
    {
 
      var app = qx.core.Init.getApplication();
      app.getAccessManager().authenticate(username, password, function(data)
      {
        if (data.error) {
          callback.call(context, false, data.error);
        }
        else
        {
          /*
           * login was successful
           */
          callback.call(context, true);
 
          /*
           * load configuration data for this user
           */
          app.getConfigManager().load();
        }
      },
      this);
    },
 
 
    /**
     * Logout
     */
    logout : function()
    {
      /*
       * call parent method to log out
       */
      this.getAccessManager().logout(function()
      {
        /*
         * reload configuration data for anonymous
         */
        this.getConfigManager().load();
      },
      this);
    },
 
 
    /**
     * Changes the backend server.
     *
     */
    _applyServer : function(version, old)
    {
 
      /*
       * remove the session of the other server if exists
       */
      if ( old ) {
        this.getStateManager().removeState('sessionId');
      }
 
      /*
       * save the state in the URL hash
       */
      this.getStateManager().setState("server", version);
 
      /*
       * set the new values according to server version
       */
      switch(version)
      {
        case "rpcphp":
          this.getRpcManager().setServerUrl("../services/server_rpcphp.php");
          this.getAccessManager().setService("simple.AuthController");
          this.getConfigManager().setService("simple.ConfigController");
          if ( old )
          {
            dialog.alert("Using RpcPhp server without database backend. Data is saved in PHP session.");  
          }
          break;
 
        case "qcl":
          this.getRpcManager().setServerUrl("../services/server.php");
          this.getAccessManager().setService("access.AuthController");
          this.getConfigManager().setService("access.ConfigController");
          if ( old )
          {
            dialog.alert("Using qcl server now with real database backend.");  
          }
 
          break;
 
        default:
          dialog.alert("Invalid server version");
      }
 
      /*
       * (re-)authenticate and when done, load new config values
       */
      this.getAccessManager().connect( function() {
        this.getConfigManager().load();
      }, this);
    }
  }
});

Let's have a look at the class. It should be pretty self-explanatory.

  • It extends qx.application.Standalone and includes the mixin qcl.application.MAppManagerProvider, which does most of the work for us. In particular, it configures the various managers used by the application.
  • It then tells the mixin where to find the jsonrpc server, and which backend rpc classes to use for authentication and configuration. It starts the authentication and when the server has responded with a session id, user data and the permissions, it goes on to load configuration data.
  • It pre-configures a login popup singleton, configuring it with a callback function that is called when the user has entered username and password.
  • Finally, a logout method initiates logout on the server and reloads configuration data for the anonymous user.

The application tool bar

Next, we need to write the toolbar component referred to in Application.xml, which contains login button that displays the login popup. Create a file ToolBar.xml in the folder components:

source/class/access/components/ToolBar.xml

<?xml version="1.0" encoding="utf-8"?>
<qxt:component
  xmlns:qx="http://www.qxtransformer.org/qooxdoo/0.8"
  xmlns:qxt="http://www.qxtransformer.org/extension/0.4"
  xmlns:qcl="http://www.qooxdoo.org/contrib/qcl/trunk"
  author=""
  className="access.components.ToolBar"
  tagName="toolBar">
 
  <qx:toolBar>
 
    <qx:toolBarPart>
 
      <qx:toolBarButton 
        id="loginButton"      
        label="Login" icon="icon/16/status/dialog-password.png"
        visibility="excluded">
 
        <!-- show button only when user is not logged in -->
        <qcl:observe 
          property="visibility" 
          source="this.getApplication().getUserManager()"
          sourceProp="activeUser"
          converter="function(activeUser){ return ( ! activeUser || activeUser.isAnonymous() ) ? 'visible' : 'excluded' }" />
 
        <!-- show login popup on button click -->
        <qxt:listener type="execute">
          qcl.ui.dialog.Dialog.show("login");
        </qxt:listener>
 
      </qx:toolBarButton>
 
      <qx:toolBarButton 
        id="logoutButton"
        label="Logout" icon="icon/16/actions/application-exit.png"
        visibility="excluded">
 
        <!-- show button only when other button is not visible and the other way round -->
        <qcl:observe 
          property="visibility" 
          source="loginButton"
          sourceProp="visibility"
          converter="function(visibility){ return ( visibility == 'visible' ) ? 'excluded' : 'visible' }" />
 
        <!-- logout user on button click -->
        <qxt:listener type="execute">
          this.getApplication().logoutUser();
        </qxt:listener>
 
      </qx:toolBarButton>
 
      <qx:atom 
        label="Loading..." 
        icon="icon/22/apps/preferences-users.png">
 
        <qcl:observe 
          property="label" 
          source="this.getApplication().getUserManager()"
          sourceProp="activeUser.fullname" />
 
      </qx:atom>
    </qx:toolBarPart>
  </qx:toolBar>
</qxt:component>

The way this works is that we create two buttons, one that is displayed when there is no authenticated user ("Login") and one that is displayed when this first button is invisible ("Logout"). A third element is an atom widget which observes the active user's fullname property.

But let's walk through the code in more detail, since QXML is not familiar to you the way a qooxdoo javascript class is:

<?xml version="1.0" encoding="utf-8"?>
<qxt:component
  xmlns:qx="http://www.qxtransformer.org/qooxdoo/0.8"
  xmlns:qxt="http://www.qxtransformer.org/extension/0.4"
  xmlns:qcl="http://www.qooxdoo.org/contrib/qcl/trunk"
  author=""
  className="access.components.ToolBar"
  tagName="toolBar">

Since this is an application component, we need to set the className and tagName accordingly (xmlns:components="access.components.*" and <components:toolBar/> in Application.xml).

  <qx:toolBar>  
    <qx:toolBarPart>
      <qx:toolBarButton 
        id="loginButton"      
        label="Login" icon="icon/16/status/dialog-password.png"
        visibility="excluded">
 
        <!-- show button only when user is not logged in -->
        <qcl:observe 
          property="visibility" 
          source="this.getApplication().getUserManager()"
          sourceProp="activeUser"
          converter="function(activeUser){ return ( ! activeUser || activeUser.isAnonymous() ) ? 'visible' : 'excluded' }" />
 
        <!-- show login popup on button click -->
        <qxt:listener type="execute">
          qcl.ui.dialog.Dialog.show("login");
        </qxt:listener>
  • The first lines model qooxdoo widget objects with their properties and should be no problem.
  • It becomes more interesting with <qcl:observe> which created code that bind the 'source' object's property specified in 'sourceProp' to the the target objets 'property'. The target here it the object represented by the parent tag, i.e. the ToolBarButton with the id 'loginButton'. <qcl:observe> is the exact opposite of <qcl:bind>, which works analogically to the qoodoo bind() method.
  • The "source" that is being observed is the user manager object, and in particular, its 'activeUser' property. This property changes whenever an authentication (login) or de-authentication(logout) occurs: it is set with the current active user object. When this object changes, the "converter" function transforms it into a string that can be the value of the 'visibility' property of the tool bar button - if the user is anonymous or there is no user, make the button visible, otherwise hide it.
  • Finally, when the login button is clicked, show the login popup.

The following lines are pretty similar for the "logout" button:

      </qx:toolBarButton>
 
      <qx:toolBarButton 
        id="logoutButton"
        label="Logout" icon="icon/16/actions/application-exit.png"
        visibility="excluded">
 
        <!-- show button only when other button is not visible and the other way round -->
        <qcl:observe 
          property="visibility" 
          source="loginButton"
          sourceProp="visibility"
          converter="function(visibility){ return ( visibility == 'visible' ) ? 'excluded' : 'visible' }" />
 
        <!-- logout user on button click -->
        <qxt:listener type="execute">
          this.getApplication().logoutUser();
        </qxt:listener>
 
      </qx:toolBarButton>
 
      <qx:atom 
        label="Loading..." 
        icon="icon/22/apps/preferences-users.png">
 
        <qcl:observe 
          property="label" 
          source="this.getApplication().getUserManager()"
          sourceProp="activeUser.fullname" />
 
      </qx:atom>
    </qx:toolBarPart>
  </qx:toolBar>
</qxt:component>

The difference is that the logout button does not observe the user manager object but simply the login button and shows or hide itself according to whether the login button is visible or not.

In addition, we add a label that observes the active user's 'fullname' property

The application body

Next is the application body. In order to keep this tutorial short, i don't go through the whole body of the sample application, but only have a look at a part of it. First, here is the full code, to be put into a file Body.xml in the components folder.

source/class/access/components/Body.xml

<?xml version="1.0" encoding="utf-8"?>
<qxt:component xmlns:qx="http://www.qxtransformer.org/qooxdoo/0.8"
  xmlns:qxt="http://www.qxtransformer.org/extension/0.4" xmlns:qcl="http://www.qooxdoo.org/contrib/qcl/trunk"
  author="" 
  className="access.components.Body"
  tagName="body">
 
  <qx:composite margin="30">
    <qx:vbox>
 
      <qx:composite >
        <qx:hbox spacing="10" qxt:flex="1">
 
          <qx:groupBox qxt:flex="1" legend="User Data">
             <!-- snip -->
          </qx:groupBox>
 
          <qx:groupBox qxt:flex="1" legend="Permission binding - with dialog demo (press the buttons)">
            <qx:vbox spacing="5">
 
              <qx:button label="Permission 'viewRecord' (Anyone)" >
                <qcl:observe property="enabled" permission="viewRecord" />
                <qxt:listener event="execute">
                   // do something
                </qxt:listener>
              </qx:button>
 
              <!-- snip -->
 
              <qx:checkBox id="condition1CheckBox" label="Condition 1">
                <qxt:listener event="changeValue" dispatchEvent="conditionsHaveChanged" />
              </qx:checkBox>
              <qx:checkBox id="condition2CheckBox" label="Condition 2">
                <qxt:listener event="changeValue" dispatchEvent="conditionsHaveChanged" />
              </qx:checkBox>
 
              <qcl:access>
                <qcl:permission name="createRecordCondition1" granted="true">
                  <qcl:dependency permission="createRecord" />
                  <qcl:updater event="conditionsHaveChanged" />
                  <qcl:condition>return condition1CheckBox.getValue()==true;</qcl:condition>
                </qcl:permission>
 
                <qcl:permission name="createRecordCondition2" granted="true">
                  <qcl:dependency permission="createRecordCondition1" />
                  <qcl:updater event="conditionsHaveChanged" />
                  <qcl:condition>return condition2CheckBox.getValue()==true;</qcl:condition>
                </qcl:permission>
 
              </qcl:access>
 
              <qx:button label="Permission 'createRecord' and condition 1" >
                <qcl:observe property="enabled" permission="createRecordCondition1" />
              </qx:button>           
 
              <qx:button label="Permission 'createRecord' and condition 1,2" >
                <qcl:observe property="enabled" permission="createRecordCondition2" />
              </qx:button>         
 
           <!-- snip -->
 
          </qx:groupBox>
 
          <qx:groupBox qxt:flex="1" legend="Configuration">
 
            <qx:vbox spacing="5">
 
              <qx:label rich="true" qxt:flex="1" width="200" allowStretchX="true">
                <qxt:property name="value">
                  <![CDATA[
                   <p>Bla bla bla</p> 
                   ]]>
                </qxt:property>
              </qx:label>          
 
              <qx:composite>
              <qx:grid spacing="5">
                <qx:column minWidth="100" />
                <qx:column flex="2"/>
 
                <qx:label value="adminValue" qxt:row="0" qxt:column="0">
                  <qcl:observe property="enabled" permission="changeAdminValue" />
                </qx:label>
                <qx:textField qxt:row="0" qxt:column="1">
                  <qcl:observe property="enabled" permission="changeAdminValue" />
                  <qcl:bind property="value" config="adminValue"  />
                </qx:textField>
 
                <!-- text value -->
                <qx:label value="userValue" qxt:row="1" qxt:column="0">
                  <qcl:observe property="enabled" permission="changeConfig" />
                </qx:label>
                <qx:textField qxt:row="1" qxt:column="1">
                  <qcl:observe property="enabled" permission="changeConfig" />
                  <qcl:bind property="value" config="userValue"  />              
                </qx:textField>
 
                 <!-- snip -->
 
              </qx:grid>
              </qx:composite>
            </qx:vbox>
          </qx:groupBox>
        </qx:hbox>
      </qx:composite>
 
       <!-- snip -->
 
    </qx:vbox>
  </qx:composite>
 
</qxt:component>

Let's only look at the interesting parts:

<qx:button label="Permission 'viewRecord' (Anyone)" >
  <qcl:observe property="enabled" permission="viewRecord" />
  <qxt:listener event="execute">
     // do something
  </qxt:listener>
</qx:button>

Here we use an observer which binds a permission to a property. What <qcl:observe> does here is to enable the button represented by the <qx:button> tag only in the case that the 'viewRecord' property is granted. How to grant properties to a user will be shown below in the backend section, this is server-side stuff.

Next, we create a couple of checkboxes to demostrate the use of conditions.

<qx:checkBox id="condition1CheckBox" label="Condition 1">
  <qxt:listener event="changeValue" dispatchEvent="conditionsHaveChanged" />
</qx:checkBox>
<qx:checkBox id="condition2CheckBox" label="Condition 2">
  <qxt:listener event="changeValue" dispatchEvent="conditionsHaveChanged" />
</qx:checkBox>

Whenever the checkboxes are (un)checked, an event is dispatched on the main widget, that is, the access.Body component object. You could also dispatch a message by using the "dispatchMessage" attribute, but for our case, an event is better suited.

We now need a couple of access rules and create new permissions that only exist on the client.

<qcl:access>
 
  <qcl:permission name="createRecordCondition1" granted="true">
    <qcl:dependency permission="createRecord" />
    <qcl:updater event="conditionsHaveChanged" />
    <qcl:condition>return condition1CheckBox.getValue()==true;</qcl:condition>
  </qcl:permission>
 
  <qcl:permission name="createRecordCondition2" granted="true">
    <qcl:dependency permission="createRecordCondition1" />
    <qcl:updater event="conditionsHaveChanged" />
    <qcl:condition>return condition2CheckBox.getValue()==true;</qcl:condition>
  </qcl:permission>
 
</qcl:access>
  • The <qcl:access> tag contains <qcl:permission> tags. Since these permissions exist only on the client, we need to manually grant them (granted="true"). Usually, permissions are created on the server and then sent to the client (and thus are granted).
  • Permissions can have dependencies. We can see here, that 'createRecordCondition1' depends on 'createRecord' and 'createRecordCondition2' on 'createRecordCondition1' and thus implicitly on 'createRecord'.
  • The state of the permissions must be updated when certain things happen in an application. This is what <qcl:updater> is for, which can listen for events (dispatched at the widget level) or messages (which are always global).
  • Finally, you can add conditions which all must be fulfilled for the permission state to be 'true'. The <qcl:condition> tag can contain inline code as in our example, or it can refer to a delegate function by using the "delegate" attribute (just as in <qxt:listener>)

Now we use the newly created permissions to control the availability of buttons:

<qx:button label="Permission 'createRecord' and condition 1" >
  <qcl:observe property="enabled" permission="createRecordCondition1" />
</qx:button>           
 
<qx:button label="Permission 'createRecord' and condition 1,2" >
  <qcl:observe property="enabled" permission="createRecordCondition2" />
</qx:button>         

You can bind widget properties not only to permissions, but also to configuration values.

<qx:label value="userValue" qxt:row="1" qxt:column="0">
  <qcl:observe property="enabled" permission="changeConfig" />
</qx:label>
<qx:textField qxt:row="1" qxt:column="1">
  <qcl:observe property="enabled" permission="changeConfig" />
  <qcl:bind property="value" config="userValue"  />              
</qx:textField>

Whenever the config value changes, the widget property value will be updated. Note that, <qcl:bind>, in contrast to <qxt:bind> automatically creates a bi-directional binding here, i.e. whenever the TextField is updated, the configuration value is, too. The change is automagically sent to the server and updated on the server as well. If you only want to update the widget property value, use <qcl:observe>

Creating the backend

Now that we have created the frontend, it is time to turn to the server backend files. For the backend, we use the trunk version of the RpcPhp server RpcPhp server, more specifically, an exended version of it from the qcl php library. This is not mandatory - as long as you return the data in the structure expected by the qcl.data.store.JsonRpc data stores, you can use the RpcPhp 1.0 server or any server you want. However, if you use the specially designed PHP backend, setting up an application backend should be quite easy, as we will see in the following.

Creating and configuring the server

First of all, we need a directory structure for the server scripts. in the application folder ('access') create the following directories:

|-services/
  |-class/
  | +-access/
  |   +-data/
  +-log/

Make sure to "chmod 0777" the "log" directory so that the backend can keep a log file.

Then we need to create the server script which serves as an entry point for the json-rpc requests and which returns the data to the client:

services/server.php

<?php
 
/*
 * Configure constants & runtime settings
 */
require "config.php";
 
/*
 * Load classes
 */
require_once "qcl/server/Server.php";
 
/*
 * Start server with paths to the service classes, i.e.
 * qcl and application classes
 */
qcl_server_Server::run( array(
  QCL_CLASS_PATH,
  APPLICATION_CLASS_PATH
) );
?>

Nothing has to be changed/configured in this file, just like in the case of the next two configuration files, which can be used verbatim.

'services/config.php' defines a couple of constants.

services/config.php

<?php
 
/*
 * set error level.
 */
error_reporting( E_ALL ^ E_NOTICE  );
 
/**
 * Path to the folder where projects from qooxdoo-contrib are
 * downloaded.
 */
define("CONTRIB_PATH", "../contrib/" ); // standard path
 
/**
 * Path to the RpcPhp package in qooxdoo-contrib
 */
define("RPCPHP_SERVER_PATH", CONTRIB_PATH . "RpcPhp/trunk/"); // standard path
 
/**
 * Path to the qcl php library
 */
define("QCL_CLASS_PATH", CONTRIB_PATH . "qcl-php/trunk/class/"); // standard path
 
/**
 * Path to the application backend classes. Usually "./class".
 */
define("APPLICATION_CLASS_PATH", "./class");
 
/*
 * Path to file where the server and application writes log messagges
 * Required. File must exist and be writable or the directory in which it
 * is located must be writable so it can be created.
 */
define( "QCL_LOG_FILE", "log/application.log" );
 
/*
 * don't touch anything beyong this point
 */
ini_set('include_path', implode(
  PATH_SEPARATOR,
  array(
    RPCPHP_SERVER_PATH,
    QCL_CLASS_PATH,
    APPLICATION_CLASS_PATH,
    ini_get("include_path")
  )
) );
?>

'services/global_settings.php' is required and used by the RpcPhp server:

services/global_settings.php

<?php
/**
 * Global settings for the JSON-RPC server
 */
 
/*
 * set error level
 */
error_reporting(E_ALL ^ E_NOTICE /* ^ E_WARNING */);
 
/*
 *  accessibility needed for test.php - change to "domain" before production use
 */
define( "defaultAccessibility", "public" );
 
/*
 * whether the server should log the request. 
 * You need this only for debugging.
 */
define("JsonRpcDebug", false );
 
/*
 * which file the server should log to
 */
define( "JsonRpcDebugFile", "log/server.log" );
?>

The only file that you actually might have to explicitly adapt is 'services/class/access/application.ini.php'. If your mysql server runs on localhost:3306 and you have created a user "test" with password "test" and a database "test" that this user can access, you can even use the file without change.

In contrast to what the ini keys suggest, admin-level and user-level access to the database is currently not separately implemented. For now, use the same user, which must have the privileges to create, modify and delete tables in the given database. If possible, grant the global "reload" privileges to this user as long as you are developing. It is planned to implement separate access for normal and maintenance operations soon.

services/class/access/application.ini.php

;;<?php exit; ?>;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Leave the above line to make sure nobody can access this file ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
[database]
 
;; database.type
;; the database type. currently, only mysql is supported and tested. Since
;; the PDO abstraction layer is used, it will be easy to support other backends.
type = mysql
 
;; database.host
;; the database host, usually localhost
host = localhost
 
;; database.port
;; the port on which the database listens for requests, ususally 3306
port = 3306
 
;; database.username and database.userpassw
;; The name and password of the user which is used to do normal database insertion
;; and update tasks. The user should not be allowed to create, alter or delete tables.
;; Currently, use the same user for both access types.
username  = test
userpassw = test
adminname  = test
adminpassw = test
 
;; database.userdb
;; The name of the databases that contains the user data
userdb = test
 
;; database.admindb
;; The name of the database holding all the tables with global and
;; administrative information. Can be the same as database.userdb,
;; but if you can access or create more than one database,
;; is recommended to keep a separate database for this.
admindb = test
 
;; database.tableprefix
;; A global prefix for all tables that are created, which makes
;; it possible to keep the data of several applications in one
;; database. you can omit this if no prefix is needed.
tableprefix = access_
 
;; database.encoding
;; The default encoding scheme of the database. It is recommended
;; to use the default utf8.
encoding  = utf8
 
[service]
 
;; service.event_transport
;; Whether the server response should contain messages and events
;; for the qooxdoo application
event_transport = yes
 
[macros]
 
;; DSN information. Since the ";" character cannot be used in value
;; definitions, it is replaced by "&" in the the dsn string.
dsn_user = "${database.type}:host=${database.host}&port=${database.port}&dbname=${database.admindb}"
dsn_admin ="${database.type}:host=${database.host}&port=${database.port}&dbname=${database.admindb}"

Creating the application script

The next task is to create the main application script. The application is a php singleton object that contains all methods that all service class controllers should be able to access, in addition to start-up and close-down methods. It will be called by the server before instantiating and calling the service class.

services/class/access/Application.php:

<?php
 
qcl_import( "qcl_application_Application" );
qcl_import( "qcl_data_model_db_PersistentObject" );
 
class access_ApplicationCache
  extends qcl_data_model_db_PersistentObject
{
  public $dataImported = false;
}
 
/**
 * Main application class
 *
 */
class access_Application
  extends qcl_application_Application
{
 
 /**
   * The path to the application ini-file
   * Can be omitted, the framework will automatically find it.
   * @var string
   */
  protected $iniPath = "access/application.ini.php";
 
  /**
   * Starts the application, does on-the-fly database setup
   * objects
   */
  public function main()
  {
    /**
     * Clear internal caches. This is only necessary during development
     * As long as you modify the properties of models.
     */
    qcl_data_model_db_ActiveRecord::resetBehaviors();
 
    /*
     * Load initial data into models if that hasn't happened yet
     */
    $cache = access_ApplicationCache::getInstance();
    if (  ! $cache->get("dataImported") )
    {
       $this->log("Importing data ....", QCL_LOG_APPLICATION );
       $this->importInitialData( array(
        'config'      => "access/data/Config.xml",
        'user'        => "access/data/User.xml",
        'permission'  => "access/data/Permission.xml",
        'role'        => "access/data/Role.xml",
       ) );
       $cache->set( "dataImported", true );
       $cache->savePersistenceData(); // usually not needed, automatically saved at shutdown
    }
  }
}
?>

The qcl_import() function provides a convenient way of loading classes by their name. The class file for "qcl_application_Application" is located in class/qcl/application/Application.php qcl_import() also loads and executes all __init__.php files it finds in the filesystem path leading to the class file.

The "main()" method of the application is automatically called before a service method is executed. As demostrated here, it can be used to do on-the-fly database setup: the application checks whether the model data is already imported and if not, imports it.

We use a persistent object here, based on the qcl_data_model_db_PersistentObject class, which persists its public properties in the database and can therefore be used to store the information that the data is already imported. You can use this cache for any kind of data that needs to be persisted.

Creating model data

Let's have a look at how the model data is defined, starting with the configuration data. We won't cover here how to create models.

The xml file contains <model> nodes with child nodes with tags that mirror their property name. The model node has a "name" attribute which contains the class name of the model. For the moment, it is not really important to understand the data, except that we are dealing with a model here that supports "named ids": in addition to a numeric id that identifies the model data record in the database, the data record has a "named id", i.e. a string that must be unique in the record table. Here, the configuration keys are used as named ids. Each record then can be accessed using the named id.

services/class/access/data/Config.xml

<?xml version="1.0" standalone="yes"?>
<root>
  <model name="qcl_config_ConfigModel">
    <data>
      <record namedId="global">
        <type>0</type>
        <default>global</default>
        <customize>0</customize>
      </record>
      <record namedId="custom">
        <type>0</type>
        <default>custom</default>
        <customize>1</customize>
      </record>
      <record namedId="number">
        <type>1</type>
        <default>1</default>
        <customize>1</customize>
      </record>
      <record namedId="list">
        <type>3</type>
        <default>foo,bar,baz</default>
        <customize>1</customize>
      </record>
    </data>
    <links>
      <relation name="Config_UserConfig" />
    </links>
  </model>
</root>

The xml configuration data has the following characteristics:

  • The property "type" can have the following values: string (type=0), number (type=1), boolean (type=2) and list (type=3). A "list" value will be converted into an array before being returned. It is stored as a string separated by commas.
  • Each configuration key has a default value to which it can be reset to.
  • Each configuration key can have custom user values if the "customize" property is set to boolean true (1 in the xml). They are stored in a separate table.

Next, the permission data.

services/class/access/data/Permission.data.xml

<?xml version="1.0" encoding="utf-8"?>
<root>
  <model name="qcl_access_model_Permission">
    <data>
      <record namedId="viewRecord"/>
      <record namedId="createRecord"/>
      <record namedId="deleteRecord"/>
      <record namedId="manageUsers"/>
      <record namedId="manageConfig"/>
    </data>
    <links>
      <relation name="Permission_Role">
        <link namedId="viewRecord">anonymous,user</link>
        <link namedId="createRecord">user</link>
        <link namedId="deleteRecord">manager</link>
        <link namedId="manageUsers">admin</link>
      </relation>
    </links>
  </model>
</root>

Pretty straightforward. The only property needed is the "namedId" property. In this code, we get to know the "relation" behavior of qcl data models. Each relation has a name and links. Each link links, as the name suggests, two model data records with each other. Here we link, for example, the permission model with named id "viewRecord" with the role model records that have the named ids "anonymous" and "user".

Now to "roles". Roles are bundles of permissions:

services/class/access/data/Role.data.xml

root>
  <model name="qcl_access_model_Role">
    <data>
      <record namedId="anonymous">
        <name>Anonymous user</name>
      </record>
      <record namedId="user">
        <name>Normal user</name>
      </record>
      <record namedId="manager">
        <name>Manager role</name>
      </record>
      <record namedId="admin">
        <name>Administrator role</name>
      </record>
    </data>
    <links>
      <relation name="Permission_Role">
        <link namedId="anonymous">viewRecord</link>
        <link namedId="user">viewRecord,createRecord</link>
        <link namedId="manager">deleteRecord</link>
        <link namedId="admin">manageUsers</link>
      </relation>
      <relation name="User_Role">
        <link namedId="user">user1,user2,user3,admin</link>
        <link namedId="manager">user3,admin</link>
        <link namedId="admin">admin</link>
      </relation>
    </links>
  </model>
</root>

Same thing going on here. We see the reverse links from the role to the permission. Note theat the name of the relation must be the same. You probably notice that there is redundancy here, since the links have already been defined in the permission data.

We also have the relation to the user model data, which is defined next:

services/class/access/data/User.data.xml

<?xml version="1.0" standalone="yes"?>
<root>
  <model name="qcl_access_model_User">
    <data>
      <record namedId="user1">
        <name>User 1</name>
        <password>user1</password>
        <active>1</active>
      </record>
      <record namedId="user2">
        <name>User 2</name>
        <password>user2</password>
        <active>1</active>
      </record>
      <record namedId="user3">
        <name>User 3</name>
        <password>user3</password>
        <active>1</active>
      </record>
      <record namedId="admin">
        <name>Administrator</name>
        <password>admin</password>
        <active>1</active>
      </record>
    </data>
    <links>
      <relation name="User_Role">
        <link namedId="user1">user</link>
        <link namedId="user2">user</link>
        <link namedId="user3">user,manager</link>
        <link namedId="admin">admin,manager,user</link>
      </relation>
    </links>
  </model>
</root>

Service Controllers

Finally, we need the json-rpc service controllers which do all the server-side work for us. Luckily, most of the functionality we need is already taken care of by the qcl library. We only need service classes which expose the methods of the library classes:

services/class/access/AuthController.php

<?php
qcl_import( "qcl_access_Service" );
 
class class_access_AuthController
  extends qcl_access_Service {}
?>

services/class/access/ConfigController.php

<?php
qcl_import( "qcl_config_Service" );
 
class class_access_ConfigController
  extends qcl_config_Service {}
?>

That's it. Nice and simple, isn't it?

Client-server databinding

How to write your own client-server databinding code is covered in a separate tutorial, which explains the controller-model architecture of the server-side qcl code and how to connect it to the databinding code on the client.

Tying it all together

Before you can use your new application, you need to transform the qxml code to javascript, and then generate the scripts (source/build) from the javascript source. Go to the top access/ directory and do

./generate.py transform # this creates javascript from qxml
./generate.py pretty # this is optional, if you want to have a look at the generated javascript
./generate.py source # or ./generate.py build if you want a build version. 

The first time you run this ./generate.py source|build will download all the dependencies from qooxdoo-contrib. This might take a long time - so be patient. After that, it should be considerably quicker. If you get any error messages during the transform or build phase, you will have to check your source code for errors. You can also ask questions on the QxTransformer google group.

Once the build steps complete successfully, you should have a shiny new access-controlled and configuration-finetuned application which you can use as a blueprint to writing your own application with qooxdoo, QxTransformer and the qcl library.

Information

Last modified:
2011/07/08 16:40 by c.boulanger

Account

 
 
A book on qooxdoo RIAs, authored  by community members
JS Tutorial, JavaScript Tutorial, JavaScript Guide, Learn JavaScript JS, How To Learn JS, Learning JavaScript
 

Bad Behavior has blocked 0 potential spam attempts in the last 7 days.