Skip to content

Application Development

wmeyer edited this page Mar 11, 2011 · 4 revisions

Up to now, we have only dealt with small examples which contained one or two functions. Real applications typically consist of a number of modules. We will show how to structure such applications.
Real applications also distinguish between a model which contains the application data and logic, and one or more views which present the data to the user. The code in the action handlers, bind procedures and validate functions can be considered part of the controller: the glue code which handles user input and typically calls functions of the model layer. We will present two approaches how to separate model and views with Roads.
For long running applications, it can be important to be able to update the code without any service interruption. This is very easy to implement with Roads.

Contents

Application Configuration

As an example for this chapter, we introduce a simple poll application. This application has two types of users: administrators and normal users. Both normal users and admins may vote on polls. But only admins can create and delete polls and designate new admins.

Running the Example Application

To start the application, feed /pollapp/OPI.oz in the Mozart OPI and go to http://localhost:8080/poll.

The default administrator account is “Admin” with the password “Admin”.

In OPI.oz, we see how an application is registered, using the URL of the compiled functor which implements the application:

{Roads.registerApplication poll 'x-ozlib://wmeyer/pollapp/PollApp.ozf'}

An application in Roads is a mapping from path elements to functors, extended by optional special purpose functions (for initialization and shutdown) and exported options. A defining characteristic for applications is that all functions of one application share the same session instances. It is possible to run multiple applications at the same time, but these applications will never share any session data.
If you use registerFunction or registerFunctor instead of registerApplication, all functions will belong to the same implicit default application.

The application definition in PollApp.oz starts with the mentioned functor mapping and setting of an option:

   Functors = unit('':'x-ozlib://wmeyer/pollapp/ShowPolls.ozf'
                   'admin':'x-ozlib://wmeyer/pollapp/Admin.ozf'
                  )
   PagesExpireAfter=0

We use absolute URLs with the scheme name “x-ozlib”. To see how this is done, take a look at makefile.oz and build.sh.

The ShowPoll functor implements voting. Its functions are available at http://localhost:8080/poll because it is assigned to the empty atom ''. Admin implements creation and deletion of polls and other administrative tasks and is mapped to http://localhost:8080/poll/admin.

From here on, the documentation consists only of a rough draft!

Init, OnRestart and ShutDown

Init: optional application function to initialize an application; although you can of course also do that with code executed in a functor at toplevel; returns a “session template”, i.e. a session object with user-defined features. The data at these features will be available in every function of the application. Example:

fun {Init}
   session(model:{Model.new})
end

OnRestart: optional application function which is called when the server is restarted. Takes the original session template and returns an updated template. Example (not part of the actual example poll application):

fun {OnRestart Session}
   session(model:{Model.updateFrom Session.model})
end

ShutDown: optional application function which is called when the server is shut down (i.e. when {Roads.shutDown} is called). Example:

proc {ShutDown Session}
   {Session.model shutDown}
end


Global options

Set with Roads.setOption.

  • logLevel: nothing, error, debug, trace; default: trace
  • expireSessionsOnRestart: bool; default: false; whether existing session shall expire when the server is restarted; interacts with the Sawhorse option keepAliveTimeout, i.e. for existing socket connections, the session will not expire on restart
  • sessionDuration: milliseconds; how long are inactive sessions kept alive?; default: 1 hour

Application-level options

Simply export the option and set the value in the functor body.

  • name: application name as used in app log messages; default: app
  • logLevel: log level for application loging; default: trace
  • pagesExpireAfter: seconds from now that pages will expire; goes into the “Expires” HTTP header; default: 0
  • useTokenInLinks: if set to false, links to procedure values will be shorter, but also slightly vulnerable to very determined CSRF attacks (the attacker still has to guess or brute-force a user-specific 32-bit value); default: true
  • mimeType: see chapter Other Features
  • charset: see chapter Other Features
  • forkedFunctions: bool; whether subordinate spaces are used; for advanced users; setting this to false makes “bind” attributes pretty much unusable but allows side effects directly in user functions. Can be overridden for specific functions by using fork(...) in action handlers and hrefs.

Functor-level options

Functor-level options override application-level options.

Before and After

Functions exported as before and after. Available both for applications and functors. If a functor does not define them, the application definitions are used.

Before takes a session object and the user function that is about to be called. It returns the function that will actually be called. Useful for authentication.

Example:

   %% Authentication
   fun {Before Session Fun}
      IsLoggedIn = {Session.memberShared user}
      LoggingIn = {Session.condGet loginInProgress false}
   in
      if IsLoggedIn orelse LoggingIn then Fun
      else %% let user log in and then show original page
         fun {$ Session} {Login Session Session.request.originalURI} end
      end
   end

After takes a session object and the result of a user function (typically a HTML document or a fragment). It returns a postprocessed version of the user function result. Useful to add content that is common for all functions of an application or a functor.

Example (simplified from the example poll app):

   %% Add list of links for logged-in users
   fun {After Session Doc}
      IsLoggedIn = {Session.memberShared user}
   in
      if {Not IsLoggedIn} then
	 html(
	    head(title("Poll application"))
	    body(Doc))
      else
	 html(
	    head(title("Show polls"))
	    body(
	       'div'(h3("Poll App")
		     hr
		     Doc
		     hr
		     'div'(a("Admin" href:url('functor':admin function:''))
			   a("View all polls"
			     href:url('functor':'' function:showAll))
			   a("Logout " # {Session.getShared user}.login
			     href:Logout)
			  )
		    )
	       )
	    )
      end
   end

Standalone applications (outside of the OPI)

Example:

functor
import
   Roads at 'x-ozlib://wmeyer/roads/Roads.ozf'
define
   {Roads.registerApplication poll 'x-ozlib://wmeyer/pollapp/PollApp.ozf'}
   {Roads.run}
end

Hot Code Swapping

On Windows, you need this patch: http://lists.gforge.info.ucl.ac.be/pipermail/mozart-hackers/2008/003072.html. Alternatively, you can replace your Mozart\platform\win32-i486\emulator.dll with the one available at http://code.google.com/p/oz-code/downloads/detail?name=emulator.zip&can=2&q==.

Just use 'x-ozlib://wmeyer/roads/TheRoads.ozf' instead of 'x-ozlib://wmeyer/roads/Roads.ozf'. If you start your application a second time, this will NOT try to create another server, but use the existing one and update the used application configuration and code for new sessions. Existing sessions will continue to use the old code (if not configured to expire on restart).

To check that it works, delete the session cookie in your browser (roadsSession), or use

{TheRoads.setSawhorseOption keepAliveTimeout 0}
{TheRoads.setOption expireSessionsOnRestart true}

Note that no stdout is available for an application that is started like this (internally uses an extra process). Therefore, Roads uses "x-ozlib://wmeyer/sawhorse/sawhorse-log/http-error.log" as the default log file in this case, which resolves to "~/.oz/<version>/cache/x-ozlib/wmeyer/sawhorse/sawhorse-log/http-error.log".

Calling the Model

  • User functions are always executed in subordinate spaces to make repeated execution of closures possible (in other words: nested user functions can read and set variables of their outer function(s) which is essential for bind attributes. Without subordinate spaces, every form using bind could only be submitted once which is against the spirit of web applications).
  • From within subordinate spaces, only a limited set of side effects are possible. For example, global variables can not be manipulated and writing into files is not possible.
  • Typical model code DOES need side effects like these.
  • It IS possible to send messages to other threads from within subordinate spaces.
  • The recommended approach is therefore to implement the model layer (or at least the interface of the model layer to the view) as active objects. We can call methods of active objects with the usual syntax. Internally, messages will be send to the thread(s) where the object(s) live.
  • For usage in subordinate spaces, there are additional requirements. We can not send unbound variables to other threads, but we can still receive a result by using the Oz system function Port.sendRecv.
  • Roads provides a function that creates a suitable active object from a class description that follows a certain protocol. See roads/appSupport/ActiveObject.oz.
  • For an example usage, see pollapp/Model.oz.

Extract:


   ...
   fun {NewModel}
      {ActiveObject.new Model init}
   end

   class Model
      feat
         db

      meth init
         self.db = {CreateDBServer}
         {self createAdmin}
      end

      meth shutDown
         {DBServer.shutDown self.db}
      end

      meth hasVotedOn(Login PollId result:Res)
         Res=
         case {self.db
               select(votedOn('*')
                      where:[[votedOn(user) '=' Login] 'AND' [votedOn(poll) '=' PollId]]
                      result:$)}
         of nil then false
         else true
         end
      end

      ...

Conclusion

We recommend to study the poll application example in depth.
The actual application interface is implemented in ShowPolls.oz and Admin.oz. The code in these functors creates dynamic HTML pages from model data in a quite simple and elegant way, using functional programming idioms and Oz’ succinct record syntax.

The following function from Admin.oz creates an unordered list of non-admin users. Each user name is rendered as a link which – when clicked – makes the user an admin and shows the updated list of non-admins:


   fun {MakeAdmin S}
      'div'(h1("Designate an Admin")
            {UL
             {Map {S.model allNonAdmins(result:$)}
              fun {$ user(login:L ...)}
                 a("User: " # L
                   href:fun {$ S}
                           {S.model makeAdmin(L)}
                           {MakeAdmin S}
                        end
                  )
              end
             }
            }
           )
   end

 

Previous: Security    Next: Other Features

Wolfgang.Meyer@gmx.net