Ocsigen

This is the tutorial for Eliom (version 0.99.2). We are currently working a lot on the documentation for version 1. Please report any error in this tutorial and send us our comments and suggestions!

Eliom is an extension for the Web server Ocsigen that allows dynamic generation of pages. It uses very new concepts making programming very different from all other Web programming tools. It allows to write a complex Web site in very few lines of code.

The old Ocsigenmod used in version 0.6.0 is now deprecated but you can still use it. Eliom is very close to Ocsigenmod. Switching to Eliom should be easy. Have a look a that page to learn quickly how to adapt your sites.

Warning: This tutorial assumes you know the Objective Caml language.

1. The basics: main services, parameters, forms, cooperative programming

Base principles

Unlike many other Web programming techniques (CGI, PHP, ...), with Eliom, you don't write one file for each URL, but a caml module (cmo or cma) for the whole Web site.

The Eliom module allows to create new entry points to your Web site, called services. Services are usually attached to an URL and usually generate a Web page. They are represented bu OCaml values, on which you must register a function that will generate a page. There are several ways to creates pages for Eliom. This tutorial is mainly using Eliom.Xhtml, a module allowing to register xhtml pages statically typed using OCaml's polymorphic variants. The XHTML.M module defines functions to construct xhtml pages using that type system. As the Eliom.Xhtml redefines some functions of XHTML.M, open the modules in this order:



open Lwt
open XHTML.M
open Eliom
open Eliom.Xhtml

Lwt (lightweight threads) is the cooperative thread library used by Ocsigen (see later).

Here is an example showing how to create a new service and register a page created with XHTML.M. Use the function Eliom.Xhtml.register_new_service:



let coucou = 
  register_new_service 
    ~url:["coucou"]
    ~get_params:unit
    (fun _ () () -> 
      return 
        (html
           (head (title (pcdata "")) [])
           (body [h1 [pcdata "Hallo!"]])))

The same, written with fully qualified values:



let coucou = 
  Eliom.Xhtml.register_new_service 
    ~url:["coucou"]
    ~get_params:Eliom.unit
    (fun _ () () -> 
      Lwt.return 
        (XHTML.M.html
          (XHTML.M.head (XHTML.M.title (XHTML.M.pcdata "")) [])
          (XHTML.M.body [XHTML.M.h1 [XHTML.M.pcdata "Hallo!"]])))

As you can see, return is a function from Lwt. Use it as this for now, and see later for more advanced use.

Now you can compile your file (here tutorial.ml) by doing:

ocamlc -I /path_to/ocsigen/ -c tutorial.ml

Replace /path_to/ocsigen/ by the directory where Ocsigen libraries are installed (that contains eliom.cma, staticmod.cmo, etc.), usually something like /usr/lib/ocaml/3.09.3/ocsigen or /usr/local/lib/ocaml/3.09.3/ocsigen or /opt/godi/lib/ocaml/site-lib/ocsigen.

Add the following lines to Ocsigen's config file (usually /etc/ocsigen/ocsigen.conf):

<host> <site dir="examples"> <eliom module="/path_to/tutoeliom.cmo" /> </site> </host>

Then run ocsigen. You should see your page at url http://your_server/examples/coucou. See this example here.

NB: See the default config file to see how to set the port on which your server is running, the user who runs it, the path of the log files, etc.

Here is a sample Makefile for your modules.

Static typing of XHTML with XHTML.M

Typing of xhtml with XHTML.M and Eliom.Xhtml is very strict and compels you to respect xhtml 1.1 standard (with some limitations). For example if you write:


(html
   (head (title (pcdata "")) [])
   (body [pcdata "Hallo"]))

You have the following error message:


This expression has type ([> `PCDATA ] as 'a) XHTML.M.elt
but is here used with type 
([< XHTML.M.block ] as 'b) XHTML.M.elt
Type 'a is not compatible with type
'b =
  [< `Address | `Blockquote | `Del | `Div | `Dl | `Fieldset
   | `Form | `H1 | `H2 | `H3 | `H4 | `H5 | `H6 | `Hr | `Ins
   | `Noscript | `Ol | `P | `Pre | `Script | `Table | `Ul ]

'b is the type of block tags (only tags allowed inside <body>), but PCDATA (i.e. raw text) is not a block tag.

In XHTML, some tags cannot be empty. For example <table> must contains at least one row. To enforce this, the table function takes two parameters: the first one is the first row, the second one is a list containig all the other rows. (same thing for <tr> <form> <dl> <ol> <ul> <dd> <select> ...) This enforces to take care separately the case of empty lists and thus respect the DTD.

A more detailed introduction to XHTML.M is available here. Take a quick look at it before continuing this tutorial.

Alternate syntax

If you prefer using a syntax closer to html, you can write:



let coucou1 = 
  Eliom.Xhtml.register_new_service 
    ~url:["coucou1"]
    ~get_params:Eliom.unit
    (fun _ () () -> 
      return
        << <html>
             <head><title></title></head>
             <body><h1>Coucou</h1></body>
           </html> >>)

To compile this syntax, you need a camlp4 syntax extension:

ocamlc -I /path_to/ocsigen/ -pp "camlp4o /path_to/ocsigen/xhtmlsyntax.cma -loc loc" -c tutorial.ml

(Replace /path_to/ocsigen/ by the directory where ocsigen is installed). See this example here.

As the syntax extension is using the same typing system as XHTML.M, You can mix the two syntaxes (see later).

Warning: The two syntaxes are not equivalent for typing. Using the syntax extension will do less verifications. For example the following code is accepted but not valid with respect to the xhtml's dtd (because <head> must contain a title):

We recommand to use preferably the functions from XHTML.M, as you will (almost) always get valid xhtml. Use the syntax extension for example to enclose already created pieces of html, and verify the validity of your pages with the W3C validator.

More info on XHTML.M.

More info on the syntax extension.

Eliom and OCamlDuce

If OCamlDuce is installed on your system, it is now possible to use it instead of XHTML.M and Eliom.Xhtml to typecheck your pages. You get a stronger type checking and more flexibility (easier to use other XML types, easier to parse incoming XML data, etc.).

To use it, make sure that you have Eliom compiled with OCamlDuce support. Then dynlink ocamlduce.cma and eliomduce.cma from the configuration file (after eliom.cma). Then use Eliomduce.Xhtml instead of Eliom.Xhtml to register your pages.

Here is an example:


open Lwt

let s =
  Eliomduce.Xhtml.register_new_service 
    ~url:[""]
    ~get_params:unit
    (fun sp () () ->
      return
        {{ <html>
             [<head> [<title> ""]
              <body> [<h1> "This page has been type checked by OCamlDuce"]] }}) 

Eliom.HtmlText

If you want to register untyped (text) pages, use the functions from Eliom.HtmlText, for example Eliom.Text.register_new_service. Example:



let coucoutext = 
  Eliom.HtmlText.register_new_service 
    ~url:["coucoutext"]
    ~get_params:Eliom.unit
    (fun sp () () -> 
      return
        ("<html>n'importe quoi "^
         (Eliom.HtmlText.a coucou sp "clic" ())^
         "</html>"))

Try it.

More examples

Services registered with register_new_service are available for all users. We call them public services.

Page generation may have side-effects:



let count = 
  let next =
    let c = ref 0 in
      (fun () -> c := !c + 1; !c)
  in
  register_new_service 
    ~url:["count"]
    ~get_params:unit
    (fun _ () () -> 
      return
        (html
         (head (title (pcdata "counter")) [])
         (body [p [pcdata (string_of_int (next ()))]])))

See this example here.

As usual in OCaml, you can forget labels when the application is total:



let hello = 
  register_new_service 
    ["dir";"hello"]  (* the url dir/hello *)
    unit
    (fun _ () () ->
      return
        (html
         (head (title (pcdata "Hello")) [])
         (body [h1 [pcdata "Hello"]])))

See this example here.

The last example shows how to define the default page for a directory. (Note that ["rep";""] is equivalent to ["rep";"index"], because some browsers do not behave well with empty links.)



let default = register_new_service ["rep";""] unit
  (fun _ () () -> 
    return
     (html
      (head (title (pcdata "")) [])
      (body [p [pcdata "default page. rep is redirected to rep/"]])))

See default.

Parameters

Typed parameters

The parameter labelled get_params indicates the type of GET parameters for the page (that is, parameters present in the URL). unit means that the page does not take any GET parameter.

Functions implementing services take three parameters. The first one has type Eliom.server_params and corresponds to server informations (user-agent, ip, current-url, etc. - see later in that section for examples of use), the second one is for GET parameters (that is, parameters in the URL) and the third one for POST parameters (parameters in the body of the HTTP request).

Here is an example of a service with GET parameters:



let writeparams _ (i1, (i2, s1)) () =  
  return
   (html
    (head (title (pcdata "")) [])
    (body [p [pcdata "You sent: ";
              strong [pcdata (string_of_int i1)];
              pcdata ", ";
              strong [pcdata (string_of_int i2)];
              pcdata " and ";
              strong [pcdata s1]]]))

let coucou_params = register_new_service 
    ~url:["coucou"]
    ~get_params:(Eliom.int "i" ** (int "ii" ** string "s"))
    writeparams


Note that the URLs of coucou and coucou_params differ only by parameters. Url http://your_server/examples/coucou will run the first one,
http://your_server/examples/coucou?i=42&ii=17&s=krokodile will run the second one.
If i is not an integer, the server displays an error-message (try to change the value in the URL).
Here, int, string and are functions defined in the Eliom module.
Warning: The infix function ( ) is to be used to construct pairs (not tuples).

The following examples shows how to create a service with "suffix" service (taking the end of the URL as a parameter, as wikis do very often) and how to get server information:



let uasuffix = 
  register_new_service 
    ~url:["uasuffix"]
    ~get_params:(suffix (int "year" ** int "month"))
    (fun sp (year, month) () -> 
      return
       (html
        (head (title (pcdata "")) [])
        (body
           [p [pcdata "The suffix of the url is ";
               strong [pcdata ((string_of_int year)^"/"
                               ^(string_of_int month))];
               pcdata ", your user-agent is ";
               strong [pcdata (Eliom.get_user_agent sp)];
               pcdata ", your IP is ";
               strong [pcdata (Eliom.get_ip sp)]]])))

This service will answer to URLs like http://.../uasuffix/2000/11.

See uasuffix

Suffix parameters have names, because we can create forms towards these services. uasuffix/2000/11 is equivalent to uasuffix/?year=2000&month=11.

suffix_prod allows to take both a suffix and other parameters.
all_suffix allows to take the end of the suffix as string list.



let isuffix = 
  register_new_service 
    ~url:["isuffix"] 
    ~get_params:(suffix_prod (int "suff" ** all_suffix "endsuff") (int "i"))
    (fun sp ((suff, endsuff), i) () -> 
      return
       (html
        (head (title (pcdata "")) [])
        (body
           [p [pcdata "The suffix of the url is ";
               strong [pcdata (string_of_int suff)];
               pcdata " followed by ";
               strong [pcdata (string_of_url_path endsuff)];
               pcdata " and i is equal to ";
               strong [pcdata (string_of_int i)]]])))

See isuffix.

The following example shows how to use your own types:



type mysum = A | B
let mysum_of_string = function
  | "A" -> A
  | "B" -> B
  | _ -> raise (Failure "mysum_of_string")
let string_of_mysum = function
  | A -> "A"
  | B -> "B"

let mytype = 
  Eliom.Xhtml.register_new_service 
    ~url:["mytype"]
    ~get_params:
      (Eliom.user_type mysum_of_string string_of_mysum "valeur")
    (fun _ x () -> 
      let v = string_of_mysum x in 
      return
        (html
         (head (title (pcdata "")) [])
         (body [p [pcdata (v^" is valid. Now try with another value.")]])))

See mytype.

Untyped parameters

If you want a service that answers to request with any parameters, use the Eliom.any value. The service will get an association list of strings. Example:



let any_serv = register_new_service 
    ~url:["any"]
    ~get_params:Eliom.any
  (fun _ l () ->
    let ll = 
      List.map 
        (fun (a,s) -> << <strong>($str:a$, $str:s$)</strong> >>) l 
    in  
    return
     << <html>
          <head><title></title></head>
          <body>
          <p>
            You sent: 
            $list:ll$
          </p>
          </body>
        </html> >>)

Try any_serv.

Catching errors

You can catch typing errors of parameters and write your own error messages using the optional parameter error_handler. Example:




let catch = register_new_service
    ~url:["catch"]
    ~get_params:(int "i")
    ~error_handler:(fun sp l -> 
      return
        (html
         (head (title (pcdata "")) [])
         (body [p [pcdata ("i is not an integer.")]])))
    (fun _ i () -> 
      let v = string_of_int i in 
      return
        (html
           (head (title (pcdata "")) [])
           (body [p [pcdata ("i is an integer: "^v)]])))

error_handler takes as parameters the usual sp, and a list of pairs (n,ex), where n is the name of the wrong parameter, and ex is the exception that has been raised while parsing its value.

See catch (change the value of the parameter).

Links

To create a link (<a>), use the function Eliom.Xhtml.a (or Eliomduce.Xhtml.a, etc), as in these examples:



let links = register_new_service ["rep";"links"] unit
 (fun sp () () -> 
   return
    (html
     (head (title (pcdata "Links")) [])
     (body 
       [p
        [Eliom.Xhtml.a coucou sp [pcdata "coucou"] (); br ();
         Eliom.Xhtml.a hello sp [pcdata "hello"] (); br ();
         Eliom.Xhtml.a default sp 
           [pcdata "default page of the dir"] (); br ();
         Eliom.Xhtml.a uasuffix sp 
           [pcdata "uasuffix"] (2007,06); br ();
         Eliom.Xhtml.a coucou_params sp 
           [pcdata "coucou_params"] (42,(22,"ciao")); br ();
         Eliom.Xhtml.a any_serv sp 
           [pcdata "any_serv"] [("sun","yellow");("sea","blue")]; br ();
         Eliom.Xhtml.a
           (new_external_service
              ~url:["http://fr.wikipedia.org";"wiki";""]
              ~get_params:(suffix (all_suffix "suff"))
              ~post_params:unit ()) 
           sp
           [pcdata "OCaml on wikipedia"]
           ["OCaml"]; br ();
         XHTML.M.a
           ~a:[a_href (uri_of_string "http://en.wikipedia.org/wiki/OCaml")]
           [pcdata "OCaml on wikipedia"]
       ]])))


See links.

If you open Eliom.Xhtml after XHTML.M, Eliom.Xhtml.a will mask XHTML.M.a. Thus you can avoid to write fully qualified values most of the time.

Eliom.Xhtml.a takes as first parameter the service you want to link to. Note that to create a (relative) link we need to know the current URL. That's why the function a takes sp as second parameter.

The third parameter is the text of the link. The last parameter is for GET parameters you want to put in the link. The type of this parameter and the name of GET parameters depend on the service you link to.

The link to Wikipedia shows how to define an external service (here it uses a suffix URL). For an external service without parameters, you can use the low level function XHTML.M.a, if you don't want to create an external service explicitely.

If you want to create (mutually or not) recursive pages, first create the service using Eliom.new_service, then register it in the table using (for example) Eliom.Xhtml.register:



let linkrec = Eliom.new_service ["linkrec"] unit ()

let _ = Eliom.Xhtml.register linkrec 
    (fun sp () () -> 
      return
       (html
        (head (title (pcdata "")) [])
        (body [p [a linkrec sp [pcdata "click"] ()]])))


See linkrec.

The server won't accept to start if there are unregistered services.

Forms

Forms towards services

The function Eliom.Xhtml.get_form allows to create a form that uses the GET method (parameters in the URL). It works like Eliom.Xhtml.a but takes as parameter a function that creates the form from parameters names.



let create_form = 
  (fun (number_name, (number2_name, string_name)) ->
    [p [pcdata "Write an int: ";
        Eliom.Xhtml.int_input ~input_type:`Text ~name:number_name ();
        pcdata "Write another int: ";
        Eliom.Xhtml.int_input ~input_type:`Text ~name:number2_name ();
        pcdata "Write a string: ";
        Eliom.Xhtml.string_input ~input_type:`Text ~name:string_name ();
        Eliom.Xhtml.string_input ~input_type:`Submit ~value:"Click" ()]])

let form = register_new_service ["form"] unit
  (fun sp () () -> 
     let f = Eliom.Xhtml.get_form coucou_params sp create_form in 
     return
       (html
         (head (title (pcdata "")) [])
         (body [f])))

See the function form in action.

Note that if you want to use typed parameters, you cannot use functions like XHTML.M.input to create your forms. Indeed, parameter names are typed to force them be used properly. In our example, number_name has type int param_name and must be used with int_input (or other widgets), whereas string_name has type string param_name and must be used with string_input (or other widgets). All functions for creating form widgets are detailed here.

For untyped forms, you may use functions from XHTML.M (or OCamlDuce's syntax, or whatever syntax you are using) or functions whose name is prefixed by "any_". Here is a form linking to our (untyped) service any_serv.



let any_form = register_new_service 
    ~url:["anyform"]
    ~get_params:unit
    (fun sp () () ->
      return
        (html
           (head (title (pcdata "")) [])
           (body 
              [h1 [pcdata "Any Form"];
               Eliom.Xhtml.get_form any_serv sp 
                 (fun () ->
                   [p [pcdata "Form to any_serv: ";
                       Eliom.Xhtml.any_input ~input_type:`Text ~name:"plop" ();
                       Eliom.Xhtml.any_input ~input_type:`Text ~name:"plip" ();
                       Eliom.Xhtml.any_input ~input_type:`Text ~name:"plap" ();
                       Eliom.Xhtml.string_input ~input_type:`Submit ~value:"Click" ()]])
                ])))

Try this form.

POST parameters

By default parameters of a Web page are in the URL (GET parameters). A web page may also expect parameters POST parameters (that is, parameters that are not in the URL but in the body of the HTTP request). Use this if you don't want the user to be able to bookmark the URL with parameters, for example if you want to post some data that will change the state of the server (paiement, database changes, etc). When designing a Web site, think carefully about the choice between GET or POST method for each service!

When you register a service with POST parameters, you must register before a service (fallback) without these parameters (for example that will answer if the page is reloaded without the hidden parameters, or if it is bookmarked).



let no_post_param_service = 
  register_new_service 
    ~url:["post"]
    ~get_params:unit
    (fun _ () () -> 
      return
        (html
         (head (title (pcdata "")) [])
         (body [h1 [pcdata 
                      "Version of the page without POST parameters"]])))
    
let my_service_with_post_params = 
  register_new_post_service
    ~fallback:no_post_param_service
    ~post_params:(string "value")
    (fun _ () value -> 
      return
        (html
         (head (title (pcdata "")) [])
         (body [h1 [pcdata value]])))

Services may take both GET and POST parameters:



let get_no_post_param_service = 
  register_new_service 
    ~url:["post2"]
    ~get_params:(int "i")
    (fun _ i () -> 
      return
        (html
         (head (title (pcdata "")) [])
         (body [p [pcdata "No POST parameter, i:";
                   em [pcdata (string_of_int i)]]])))

let my_service_with_get_and_post = register_new_post_service 
  ~fallback:get_no_post_param_service
  ~post_params:(string "value")
  (fun _ i value -> 
    return
      (html
         (head (title (pcdata "")) [])
         (body [p [pcdata "Value: ";
                   em [pcdata value];
                   pcdata ", i: ";
                   em [pcdata (string_of_int i)]]])))

POST forms

To create a POST form, use the Eliom.Xhtml.post_form function. It is similar to Eliom.Xhtml.get_form with an additional parameter for the GET parameters you want to put in the URL (if any). Here form2 is a page containing a form to the service post (using XHTML.M's functions) and form3 (defined using the syntax extension) contains a form to post2, with a GET parameter. form4 is a form to an external page.



let form2 = register_new_service ["form2"] unit
  (fun sp () () -> 
     let f =
       (Eliom.Xhtml.post_form my_service_with_post_params sp
          (fun chaine -> 
            [p [pcdata "Write a string: ";
                string_input ~input_type:`Text ~name:chaine ()]]) ()) in
     return
       (html
         (head (title (pcdata "form")) [])
         (body [f])))

let form3 = register_new_service ["form3"] unit
  (fun sp () () ->
     let f  = 
       (Eliom.Xhtml.post_form my_service_with_get_and_post sp
          (fun chaine -> 
            <:xmllist< <p> Write a string: 
                    $string_input ~input_type:`Text ~name:chaine ()$ </p> >>)
          222) in 
     return
       << <html>
            <head><title></title></head>
            <body>$f$</body></html> >>)

let form4 = register_new_service ["form4"] unit
  (fun sp () () ->
     let f  = 
       (Eliom.Xhtml.post_form
          (new_external_service 
             ~url:["http://www.petizomverts.com"]
             ~get_params:(int "i")
             ~post_params:(string "chaine") ()) sp
          (fun chaine -> 
            <:xmllist< <p> Write a string: 
                     $string_input ~input_type:`Text ~name:chaine ()$ </p> >>)
          222) in 
     return
       (html
        (head (title (pcdata "form")) [])
        (body [f])))

See the urls: post without parameter, post2 without POST parameter, form2, form3, form4.

Threads

Remember that a Web site written with Eliom is an OCaml application. This application must be able to handle several requests at the same time, if one of the requests takes time. To make this possible, Ocsigen is using cooperative threads, implemented in monadic style by Jérôme Vouillon (Lwt module), which make them really easy to use.

With respect to preemptive threads, cooperative threads are not using a scheduler to distribute processor time between threads. Instead of this, each thread must tell the others that he wants to let them work. If a thread does not cooperate, the others will be blocked.

Advantages
  • It is much lighter
  • No need of mutex and no risk of deadlock!
  • The use of many (small) threads make implementation very easy (for example, for user interfaces, no need to implement another event loop, make a thread for each widget!)
Drawbacks
  • Threads must cooperate ... Otherwise the whole program will hang.As it does not cooperate, the following page will stop the server for 5 seconds. No one will be able to do a request during this delay:


let looong =
  register_new_service
    ~url:["looong"]
    ~get_params:unit
    (fun sp () () ->
      Unix.sleep 5;
      return
        (html
          (head (title (pcdata "")) [])
          (body [h1 [pcdata "Ok now, you can read the page."]])))

To solve this problem, use a cooperative version of sleep:



let looong = 
  register_new_service 
    ~url:["looong"]
    ~get_params:unit
    (fun sp () () -> 
      Lwt_unix.sleep 5.0 >>= fun () ->
      return
        (html
          (head (title (pcdata "")) [])
          (body [h1 [pcdata 
                   "Ok now, you can read the page."]])))

<p class="importantwarning"> The binary operator >>= used to bind the result of a non blocking computation to another. In other words, it means: "if the left handside takes time, do not block here, continue to the next instruction, but remember to come back here and give the result to the following function once you get it".

In other words, it is used to specify a sequence of computations that depend one from another. It is a kind of let binding. e1 >>= (fun r -> return e2) will try to evaluate e1, and once e1 is evaluated, it will give the result to the function given as second parameter. If the left handside (e1) takes time (for example because it is waiting for a read on a socket), the whole computation will be saved in a table and the program will continue to the next instruction that does not depend on e1. The computation will resume at a future cooperation point, if it is ready to continue. Instead of e1 >>= (fun r -> return e2), you can write bind e1 (fun r -> return e2).

See looong.

Lwt.bind, (or >>=) has type
'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t

Lwt.return has type
'a -> 'a Lwt.t

'a Lwt.t is the type of threads returning a result of type 'a. All cooperative functions must return this type.

Cooperation points are inserted when you call cooperative functions such as Lwt_unix.read or Lwt_unix.write. You can add other cooperation points by calling Lwt_unix.yield (). The thread will suspend itself, Lwt will wake up the oldest waiting thread, and this thread will resume as soon as possible.

Monadic cooperative threads are not difficult to use once you get used to think the following way: programming is not putting instructions one after another any more, but defining a dependency relation (>>=) between function calls. Remember:

  • Functions that may take time to complete always return something of type &alpha; Lwt.t (where &alpha; is any type). They are called cooperative functions.
  • The only way to use the result of such a function is to bind it to another cooperative function (what to do after) using >>=.

Exceptions

Use Lwt.fail and Lwt.catch inside threads instead of raise and try ... with.

To raise an exception e inside a Lwt thread, use fail e and be carefull about functions that may raise exceptions.

You must be careful when catching exception with Lwt. If you use the try ... with construct for an expression of type 'a Lwt.t, it may not work (as the computation may happen later).

Remember the following: if e has type 'a Lwt.t (where 'a is any type), do not write:


try
  e
with
  ...

but write:


catch
  (fun () -> e)
  (function ... | exn -> fail exn)

What if my function is not implemented in cooperative way?

If my function is thread-safe (for preemptive threads)

Ocsigen implements a way to make a non cooperative computation be executed automatically by a another preemptive thread (for example a database request using a non-cooperative database library, such as postgresql-ocaml or pgocaml). To do this, use the detach function. For example:



let looong2 = 
  register_new_service 
    ~url:["looong2"]
    ~get_params:unit
    (fun sp () () -> 
      Preemptive.detach Unix.sleep 5 >>= fun () ->
      return
        (html
          (head (title (pcdata "")) [])
          (body [h1 [pcdata 
                   "Ok now, you can read the page."]])))

See looong2.

A pool of preemptive threads is waiting for such "detached functions". You can specify the number of threads in the pool in the configuration file.

Warning: Detached functions must be thread-safe! Be careful to concurrent access to data. Be sure to use mutexes for your own functions, and use only thread-safe libraries.<!– For example <code></code> (version ) is NOT thread-safe, <code></code> (version ) is thread-safe. –> The libraries from Ocsigen are NOT thread-safe for now. Let us know if you really need them to be thread-safe.

If my function is not thread-safe (for preemptive threads)

If you want to use a function that takes time to execute but it not written in thread-safe way (for example some functions of OCaml's Str module), consider rewriting it in cooperative manner, or delegate the work to another process.

Examples

A thread that prints "hello" every 10 seconds

Just add the following lines to your program:


let rec f () = 
  print_endline "hello";
  Lwt_unix.sleep 10. >>= f
in f ();
      
More advanced use: Create a thread waiting for an event

Lwt.wait () creates a thread that waits forever. You can wake it up using Lwt.wakeup.


(* Create the event *)
let w = wait () in

(* Bind a thread on this event *)
(w >>= (fun v -> return (print_endline v));
...

(* Trigger the event *)
wakeup w "HELLO");
(* All threads waiting for w are awoken, and w's value is "HELLO". *)
      

The big picture

You now have the minimum knowledge to write basic Web sites with Eliom: typing of pages, creation of services, parameters, forms and database acces using Lwt (and possibly Preemptive.detach). Here is a summary of all other concepts introduced by Eliom. They will enable you to program easily more complex behaviours. They will be developped in the following of this tutorial.

Different kinds of services

Before beginning the implementation, think about the URLs you want to create as entry points to your Web site, and the services you want to provide.

Services we used so far are called main services. Actually, Eliom uses three kinds of services:

Main services
are the main entry points of your sites. Created by new_service or new_post_service. They correspond to the public URLs of your Web site, and will last forever.
(Attached) coservices
are services that share their location (URL) with a main service (fallback). They are distinguished from that main service using a special parameter (added automatically by Eliom). They are often created dynamically for one user (often in the session table), depending on previous interaction during the session. They often disappear after a timeout letting the fallback answer afterwards. Another use of (POST) coservices is to particularize one button but not the page it leads to (like the disconnect button in the example of sessions with actions below).
Non-attached coservices
are coservices that are not attached to a particular URL. A link towards a non-attached coservice will go to the current URL (with special parameters allowing to recognize the coservice). It is usefull when you want the same link or form on several pages (for example a connection box) but you don't want to go to another URL. Non-attached coservice are ofetn used with actions (see below).====GET or POST? Each of these services has both a version with GET parameters and a version with POST parameters.
POST and GET parameters is not equivalent, and you must be very carefull if you want one or the other.
GET parameters are the parameters you see in the URL (for example http://your_server/examples/coucou?i=42&ii=17&s=krokodile). They are created by browser if you tell forms to use the GET method, or written directly in the URL. Parameters are sent by browsers in the body of the HTTP request when the form is using the POST method. That's the only solution if you want to send files with your request.

Remember that only pages without POST parameters are bookmarkable. Use GET parameters if you want the user be able to come back to the URL later or to write the URL manually.
Use POST parameters when you want a different behaviour between the first click and a reload of the page. Usually sending POST parameters triggers an action on server side (like a paiement, or adding something in a database), and you don't want it to succeed several times if the page is reloaded or bookmarked.

Data returned by services

Services can send several types of data, using these different modules:

ServicesAttached coservicesNon attached coservices
Eliom.Xhtmlallows to register functions that generate xhtml pages statically checked using polymorphic variant types. You may use constructor functions from XHTML.M or a syntax extension close to the standard xhtml syntax.
Eliom.Blocksallows to register functions that generate a portion of page (content of body tag) using XHTML.M or the syntax extension. (usefull for XMLHttpRequest requests for example).
Eliomduce.Xhtmlallows to register functions that generate xhtml pages statically checked using OCamlduce. Typing is more strict, but you need a modified version of the OCaml compiler (OCamlduce).
Eliom.HtmlTextAllows to register functions that generate text html pages, without any typechecking of the content. The content type sent by the server is "text/html".
Eliom.CssTextAllows to register functions that generate CSS pages, without any typechecking of the content. The content type sent by the server is "text/css".
Eliom.TextAllows to register functions that generate text pages, without any typechecking of the content. The services return a pair of strings. The first one is the content of the page, the second one is the content type.
Eliom.Actionsallows to register actions, that is functions that do not generate any page. The URL is reloaded after the action.
Eliom.Unitis like Eliom.Actions but the URL is not reloaded after the action.
Eliom.Redirectionsallows to register HTTP redirections. You register the URL of the page you want to redirect to. The browser will get a 301 code in answer and redo the request to the new URL.
Eliom.Filesallows to register services that send files
Eliom.Anyallows to register services that can choose what they send, for example an xhtml page or a file, depending on some situation (parameter, user logged or not, page present in a cache ...).It is also possible to create your own modules for others types of pages.

Public and session service tables

Each of these registrations may be done in the public service table, or in a session service table, accessible only for one user of the Web site. This allows to create services tailor-made for one user.

Eliom will try to find the page, in that order:

  • in the session service table,
  • in the public service table,
  • the fallback in the session table, if the coservice has expired,
  • the fallback in the public table, if the session has expired.

Session data tables

It is also possible to create a session data table, where you can save information about the session. Each service can look in that table to see if a session is opened or not and get the data.

Example cases

Here is a list of frequent cases and the solution Eliom provides to to solve them. Most of them will be developped in the following.

Display the result of a search (plane ticket, search engines ...)
Use a coservice (with timeout) in the session service table
Keep information about the session (name of the user ...)
Use a session data table
A connection or disconnection box on each page of your site
Use an action registered on a non-attached coservice.
Add something in a shopping basket
Use an action registered on a non-attached coservice, with the name of the items as parameter. The action saves the shopping basket in a session data table. Thus, the shopping basket will remain even if the user pushes the back button of the browser.
Book a ticket (in several steps)
Each step creates new (GET) coservices (with or without parameters, all attached to the service displaying the main booking page) according to the data entered by the user. These coservices are registered in the session table (with a timeout for the whole session or for each of them). Thus the user can go back to a previous situation, or keep several proposals on differents tabs before choosing one.
...
Help us to complete this list by giving your examples or asking questions about other cases! Thank you!

2. Sessions, users and other common situations in Web sites

When programming dynamic Web sites, you often want to personalise the behaviour and content for one user. To do this, you need to recognise the user and save and restore its data. Eliom implements several high level features to do that:

  • Session data tables,
  • Session service tables, where you can save private versions of main services or new coservices,
  • Coservices, that may be created dynamically with respect to previous interaction with the user.

Eliom is using cookies to recognize users. One cookie is automatically set for each user when needed and detroyed when the session is closed.

Coservices, but also actions, are also means to control precisely the behaviour of the site and to implement easily very common situations that require a lot of programming work with other Web programming tools. We'll have a lot at some examples in that section.

Session data

Eliom provides a way to save session data on server side and restore it at each request. This data is available during the whole duration of the session. To save session data, create a table using Eliom.create_table and save and get data from this table using Eliom.set_session_data and Eliom.get_session_data. The following example shows a site with authentification. The name of the user is asked in the login form and saved in a table to be displayed on the page instead of the login form while the user is connected. Note that the session is opened automatically when needed.



(************************************************************)
(************ Connection of users, version 1 ****************)
(************************************************************)


(* "my_table" will be the structure used to store
   the session data (namely the login name): *)

let my_table = Eliom.create_table ()


(* -------------------------------------------------------- *)
(* Create services, but do not register them yet:           *)

let session_data_example =
  Eliom.new_service
    ~url:["sessdata"]
    ~get_params:Eliom.unit
    ()

let session_data_example_with_post_params =
  Eliom.new_post_service
    ~fallback:session_data_example
    ~post_params:(Eliom.string "login")
    ()

let session_data_example_close =
  Eliom.new_service
    ~url:["close"]
    ~get_params:Eliom.unit
    ()



(* -------------------------------------------------------- *)
(* Handler for the "session_data_example" service:          *)

let session_data_example_handler sp _ _  =
  let sessdat = Eliom.get_session_data my_table sp in
  return
    (html
       (head (title (pcdata "")) [])
       (body 
          [
           match sessdat with
           | Some name -> 
               p [pcdata ("Hello "^name); 
                  br (); 
                  Eliom.Xhtml.a 
                    session_data_example_close
                    sp [pcdata "close session"] ()]
           | None -> 
               Eliom.Xhtml.post_form 
                 session_data_example_with_post_params
                 sp
                 (fun login -> 
                   [p [pcdata "login: ";
                       Eliom.Xhtml.string_input
                         ~input_type:`Text ~name:login ()]]) ()
         ]))




(* -------------------------------------------------------- *)
(* Handler for the "session_data_example_with_post_params"  *)
(* service with POST params:                                *)

let session_data_example_with_post_params_handler sp _ login =
  Eliom.close_session ~sp >>= fun () ->
  Eliom.set_session_data my_table sp login;
  return
    (html
       (head (title (pcdata "")) [])
       (body 
          [p [pcdata ("Welcome " ^ login ^ ". You are now connected."); 
              br ();
              Eliom.Xhtml.a session_data_example sp [pcdata "Try again"] ()
            ]]))



(* -------------------------------------------------------- *)
(* Handler for the "session_data_example_close" service:    *)

let session_data_example_close_handler sp () () =
  let sessdat = Eliom.get_session_data my_table sp in
  Eliom.close_session ~sp >>= fun () -> 
  return
    (html
       (head (title (pcdata "Disconnect")) [])
       (body [
        (match sessdat with
        | None   -> p [pcdata "You were not connected."]
        | Some _ -> p [pcdata "You have been disconnected."]);
        p [Eliom.Xhtml.a session_data_example sp [pcdata "Retry"] () ]]))


(* -------------------------------------------------------- *)
(* Registration of main services:                           *)

let () =
  Eliom.Xhtml.register 
    session_data_example_close session_data_example_close_handler;
  Eliom.Xhtml.register 
    session_data_example session_data_example_handler;
  Eliom.Xhtml.register 
    session_data_example_with_post_params
    session_data_example_with_post_params_handler




See this example here.

To close a session, use the function close_session. Session data will disappear when the session is closed (explicitely or by timeout). Warning: if your session data contains opened file descriptors, they won't be closed by OCaml's garbage collector. Close it yourself! (for example using Gc.finalise).

We will see in the following of this tutorial how to improve this example to solve the following problems:

  • The use of a main service for disconnection is not a good idea for ergonomics. You probably want to go to the same page with the login form. We will do this with a coservice.
  • If you want the same login form on several pages, it is tedious work to create a coservice with POST parameters for each page. We will se how to solve this using actions and non-attached coservices.
  • Session data are kept in memory and will be lost if you switch off the server, which is bad if you want long duration sessions. You can solve this problem by using persistent tables.

Sessions services

Eliom allows to replace a public service by a service valid only for one user. Use this to personalise main services for one user (or to create new coservices available only to one user, see later). To create a "session service", register the service in a "session service table" (valid only for one client) instead of the public table. To do that, use register_for_session.

Users are recognized automatically using a cookie. Use this for example if you want two versions of each page, one public, one for connected users.
To close a session, use close_session. Both the session service table and the session data table for that user will disappear when the sesison is closed.

Note that register_for_session and close_session take sp as parameter (because sp contains the session table).

The following example shows how to reimplement the previous one (session_data_example), without using Eliom.set_session_data. Note that this version is less efficient than the other if your site has lots of pages, because it requires to register all the new services each time a user logs in. But in other cases, that feature is really usefull, for example with coservices (see later).

We first define the main page, with a login form:



(************************************************************)
(************ Connection of users, version 2 ****************)
(************************************************************)

(* -------------------------------------------------------- *)
(* Create services, but do not register them yet:           *)

let session_services_example = 
  Eliom.new_service
    ~url:["sessionservices"] 
    ~get_params:Eliom.unit 
    ()

let session_services_example_with_post_params = 
  Eliom.new_post_service 
    ~fallback:session_services_example
    ~post_params:(Eliom.string "login")
    ()

let session_services_example_close = 
  Eliom.new_service
    ~url:["close2"]
    ~get_params:Eliom.unit
    ()


(* ------------------------------------------------------------- *)
(* Handler for the "session_services_example" service:           *)
(* It displays the main page of our site, with a login form.     *)

let session_services_example_handler sp () () = 
  let f = 
    Eliom.Xhtml.post_form 
      session_services_example_with_post_params 
      sp
      (fun login -> 
        [p [pcdata "login: ";
            string_input ~input_type:`Text ~name:login ()]]) () 
  in 
  return
    (html
       (head (title (pcdata "")) [])
       (body [f]))


(* ------------------------------------------------------------- *)
(* Handler for the "session_services_example_close" service:     *)

let session_services_example_close_handler sp () () =
  Eliom.close_session ~sp >>= fun () ->
  return
    (html
       (head (title (pcdata "Disconnect")) [])
       (body [p [pcdata "You have been disconnected. ";
                 a session_services_example
                   sp [pcdata "Retry"] ()
               ]]))


When the page is called with login parameters, it runs the function launch_session that replaces some services already defined by new ones:



(* ------------------------------------------------------------- *)
(* Handler for the "session_services_example_with_post_params"   *)
(* service:                                                      *)

let launch_session sp () login =

  (* New handler for the main page: *)
  let new_main_page sp () () = 
    return
      (html
       (head (title (pcdata "")) [])
       (body [p [pcdata "Welcome ";
                 pcdata login; 
                 pcdata "!"; br ();
                 a coucou sp [pcdata "coucou"] (); br ();
                 a hello sp [pcdata "hello"] (); br ();
                 a links sp [pcdata "links"] (); br ();
                 a session_services_example_close
                   sp [pcdata "close session"] ()]]))
  in

  (* If a session was opened, we close it first! *)
  Eliom.close_session ~sp >>= fun () ->

  (* Now we register new versions of main services in the
     session service table: *)
  Eliom.Xhtml.register_for_session 
    ~sp
    ~service:session_services_example
    (* service is any public service already registered,
       here the main page of our site *)
    new_main_page;
  
  Eliom.Xhtml.register_for_session 
    ~sp
    ~service:coucou
    (fun _ () () -> 
      return
        (html
         (head (title (pcdata "")) [])
         (body [p [pcdata "Coucou ";
                   pcdata login;
                   pcdata "!"]])));

  Eliom.Xhtml.register_for_session 
    ~sp
    ~service:hello
    (fun _ () () -> 
      return
        (html
         (head (title (pcdata "")) [])
         (body [p [pcdata "Ciao ";
                   pcdata login;
                   pcdata "!"]])));

  new_main_page sp () ()
    
(* -------------------------------------------------------- *)
(* Registration of main services:                           *)

let () = 
  Eliom.Xhtml.register
    ~service:session_services_example
    session_services_example_handler;
  Eliom.Xhtml.register
    ~service:session_services_example_close
    session_services_example_close_handler;
  Eliom.Xhtml.register
    ~service:session_services_example_with_post_params
    launch_session


See the result.

Warning: As in the previous example, to implement such connection and disconnection forms, you get more flexibility by using actions instead of xhtml services (see below for the same example with actions).

Services registered in session tables are called session or private services. Services registered in the public table are called public.

Coservices

A coservice is a service that uses the same URL as a main service, but generates another page. They are distinguished from main services only by a special parameter, called state parameter. Coservices may use GET or POST parameters.

Most of the time, GET coservices are created dynamically with respect to previous interaction with the user and are registered in the session table. They allow to give a precise semantics to the "back" button of the browser (be sure that you will go back in the past) or bookmarks, or duplication of the browser's window. (See the calc example below).

Use POST coservices if you want to particularize a link or form, but not the URL it points to. More precisely, POST coservices are mainly used in two situations:

  • For the same purpose as GET coservices (new services corresponding to precise points of the interaction with the user) but when you don't want this service to be bookmarkable.
  • To create a button that leads to a service after having performed a side-effect. For example a disconnection button that leads to the main page of the side, but with the side effect of disconnecting the user.

To create a coservice, use new_coservice and new_post_coservice. Like register_new_post_service, they take a public service as parameter (labelled fallback) to be used as fallback when the user comes back without the state parameter (for example if it was a POST coservice and/or the coservice has expired).

The following example shows the difference between GET coservices (bookmarkable) and POST coservices:



(************************************************************)
(************** Coservices. Basic examples ******************)
(************************************************************)

(* -------------------------------------------------------- *)
(* We create one main service and two coservices:           *)

let coservices_example = 
  Eliom.new_service
    ~url:["coserv"] 
    ~get_params:Eliom.unit
    ()

let coservices_example_post = 
  Eliom.new_post_coservice 
    ~fallback:coservices_example 
    ~post_params:Eliom.unit
    ()

let coservices_example_get = 
  Eliom.new_coservice
    ~fallback:coservices_example
    ~get_params:Eliom.unit
    ()


(* -------------------------------------------------------- *)
(* The three of them display the same page,                 *)
(* but the coservices change the counter.                   *)

let _ = 
  let c = ref 0 in
  let page sp () () = 
    let l3 = Eliom.Xhtml.post_form coservices_example_post sp 
        (fun _ -> [p [Eliom.Xhtml.string_input 
                        ~input_type:`Submit
                        ~value:"incr i (post)" ()]]) () 
    in
    let l4 = Eliom.Xhtml.get_form coservices_example_get sp 
        (fun _ -> [p [Eliom.Xhtml.string_input
                        ~input_type:`Submit
                        ~value:"incr i (get)" ()]]) 
    in 
    return
      (html
       (head (title (pcdata "")) [])
       (body [p [pcdata "i is equal to ";
                 pcdata (string_of_int !c); br ();
                 a coservices_example sp [pcdata "reload"] (); br ();
                 a coservices_example_get sp [pcdata "incr i"] ()];
              l3;
              l4]))
  in
  Eliom.Xhtml.register coservices_example page;
  let f sp () () = c := !c + 1; page sp () () in
  Eliom.Xhtml.register coservices_example_post f;
  Eliom.Xhtml.register coservices_example_get f

Try coserv.

Note that if the coservice does not exist (for example it has expired), the fallback is called.

In this example, coservices do not take any parameters (but the state parameter), but you can create coservices with parameters. Note that the fallback of a GET coservice cannot take parameters. Actually as coservices parameters have special names, it is possible to use a "pre-applied" service as fallback (see later).

Exercise: Rewrite the example of Web site with connection (session_data_example, with session data) using a POST coservice without parameter to make the disconnection link go back to the main page of the site instead of a "disconnection" page. It is better for ergonomics, but it would be even better to stay on the same page ... How to do that with POST coservices? A much better solution will be seen in the section about actions and non-attached coservices.

URLs

While designing a Web site, think carefully about the URLs you want to use. URLs are the entry points of your site. Think that they may be bookmarked. If you create a link, you want to go to another URL, and you want a page to be generated. That page may be the default page for the URL (the one you get when you go back to a bookmarked page), or another page, that depends on the precise link or form you used to go to that URL (link to a coservice, or page depending on post data). Sometimes, you want that clicking a link or submitting a form does something without changing the URL. You can do this using non-attached coservices (see below).

Continuations

Eliom is using the concept of continuation. A continuation represents the future of a program (what to do after). When a user clicks on a link or a form, he chooses the future of the computation. When he uses the "back" button of the browser, he chooses to go back to an old continuation. Continuations for Web programming have been introduced by Christian Queinnec, and are a big step in the understanding of Web interaction.

Some programming languages (Scheme...) allow to manipulate continuations using control operators (like call/cc). The style of programming used by Eliom is closer to Continuation Passing Style (CPS), and has the advantage that it does not need control operators, and fits very well Web programming.

Coservices allow to create dynamically new continuations that depend on previous interactions with users (See the calc example below). Such a behaviour is difficult to simulate with traditional Web programming.

If you want continuations dedicated to a particular user register them in the session table.

Non-attached coservices

Non-attached coservices are services that are not attached to an URL. When you point a link or a form towards such a service, the URL does not change. The name of the service is sent as a special parameter.

As for attached coservices, there are GET and POST versions. To create them, use Eliom.new_coservice' or Eliom.new_post_coservice'. POST non-attached coservices are really usefull if you want a link or form to be present on every page but you don't want the URL to change. Very often, POST coservices are used with actions (see more details and an example in the section about actions below).

Coservices in session tables

You can register coservices in session tables to create dynamically new services dedicated to an user. Here is an example of pages that add two integers. Once the first number is sent by the user, a coservice is created and registered in the session table. This service takes the second number as parameter and displays the result of the sum with the first one. Try to duplicate the pages and/or to use the back button of your navigator to verify that it has the expected behaviour.



(************************************************************)
(*************** calc: sum of two integers ******************)
(************************************************************)

(* -------------------------------------------------------- *)
(* We create two main services on the same URL,             *)
(* one with a GET integer parameter:                        *)

let calc = 
  new_service
    ~url:["calc"]
    ~get_params:unit
    ()

let calc_i = 
  new_service 
    ~url:["calc"]
    ~get_params:(int "i")
    ()


(* -------------------------------------------------------- *)
(* The handler for the service without parameter.           *)
(* It displays a form where you can write an integer value: *)

let calc_handler sp () () =   
  let create_form intname = 
    [p [pcdata "Write a number: ";
        Eliom.Xhtml.int_input ~input_type:`Text ~name:intname ();
        br ();
        Eliom.Xhtml.string_input ~input_type:`Submit ~value:"Send" ()]]
  in
  let f = Eliom.Xhtml.get_form calc_i sp create_form in
  return
    (html 
       (head (title (pcdata "")) [])
       (body [f]))





(* -------------------------------------------------------- *)
(* The handler for the service with parameter.              *)
(* It creates dynamically and registers a new coservice     *)
(* with one GET integer parameter.                          *)
(* This new coservice depends on the first value (i)        *)
(* entered by the user.                                     *)

let calc_i_handler sp i () = 
  let create_form is = 
    (fun entier ->
       [p [pcdata (is^" + ");
           int_input ~input_type:`Text ~name:entier ();
           br ();
           string_input ~input_type:`Submit ~value:"Sum" ()]])
  in
  let is = string_of_int i in
  let calc_result = 
    register_new_coservice_for_session
      ~sp
      ~fallback:calc
      ~get_params:(int "j")
      (fun sp j () -> 
        let js = string_of_int j in
        let ijs = string_of_int (i+j) in 
        return
          (html
             (head (title (pcdata "")) [])
             (body
                [p [pcdata (is^" + "^js^" = "^ijs)]])))
  in
  let f = get_form calc_result sp (create_form is) in 
  return
    (html
       (head (title (pcdata "")) [])
       (body [f]))


(* -------------------------------------------------------- *)
(* Registration of main services:                           *)

let () =
  Eliom.Xhtml.register calc   calc_handler;
  Eliom.Xhtml.register calc_i calc_i_handler

See the result.

Actions

Actions are services that do not generate any page. Use them to perform an effect on the server (connection/disconnection of a user, adding something in a shopping basket, delete a message in a forum, etc.). The page you link to is redisplayed after the action. For ex, when you have the same form (or link) on several pages (for ex a connection form), instead of making a version with post params of all these pages, you can use only one action, registered on a non-attached coservice. To register actions, just use the module Actions instead of Eliom.Xhtml (or Eliomduce.Xhtml, etc.). For example Actions.register, Actions.register_new_service, Actions.register_for_session.

Here we rewrite the example session_data_example using actions and non-attached coservices (note the POST coservice for disconnection, much better than the previous solution that was using another URL).



(************************************************************)
(************ Connection of users, version 3 ****************)
(************************************************************)


(* -------------------------------------------------------- *)
(* We create one main service and two (POST) actions        *)
(* (for connection and disconnection)                       *)

let action_example = 
  Eliom.new_service
    ~url:["action"] 
    ~get_params:Eliom.unit
    ()

let connect_action = 
  Eliom.new_post_coservice'
    ~post_params:(Eliom.string "login")
    ()
    
(* As the handler is very simple, we register it now: *)
let disconnect_action = 
  Actions.register_new_post_coservice'
    ~post_params:Eliom.unit 
    (fun sp () () -> 
      Eliom.close_session ~sp >>= fun () -> 
      return [])


(* -------------------------------------------------------- *)
(* login ang logout boxes:                                  *)

let disconnect_box sp s = 
  Eliom.Xhtml.post_form disconnect_action sp 
    (fun _ -> [p [Eliom.Xhtml.string_input
                    ~input_type:`Submit ~value:s ()]]) ()

let login_box sp = 
  Eliom.Xhtml.post_form connect_action sp
    (fun loginname ->
      [p 
         (let l = [pcdata "login: "; 
                   Eliom.Xhtml.string_input
                     ~input_type:`Text ~name:loginname ()]
         in  l)
     ])
    ()



(* -------------------------------------------------------- *)
(* Handler for the "action_example" service (main page):    *)

let action_example_handler sp () () = 
  let sessdat = Eliom.get_session_data my_table sp in
  return
    (html
       (head (title (pcdata "")) [])
       (body 
          (match sessdat with
          | Some name ->
              [p [pcdata ("Hello "^name); br ()];
              disconnect_box sp "Close session"]
          | None -> [login_box sp]
          )))
    

(* -------------------------------------------------------- *)
(* Handler for connect_action (user logs in):               *)

let connect_action_handler sp () login =
  close_session ~sp >>= fun () -> 
  Eliom.set_session_data my_table sp login;
  return []


(* -------------------------------------------------------- *)
(* Registration of main services:                           *)

let () =
  Eliom.Xhtml.register ~service:action_example action_example_handler;
  Actions.register ~service:connect_action connect_action_handler

Note that actions return a list (here empty). See later for more advanced use.

That version of the site with connection solves the main problems of sessdata:

  • Connection and disconnection stay on the same page,
  • If you want a connection/disconnection form on each page, no need to create a version with POST parameters of each service.

We'll see later how to display an error message if the connection goes wrong, and how to have persistent sessions (that stay opened even if the server is re-launched).

Details on service registration:

  • All services created during initialisation must be registered in the public table during the initialisation phase of your module. If not, the server will not start (with an error message in the logs). Thus, there will always be a service to answer when somebody clicks on a link or a form.
  • Services may be registered in the public table after initialisation with register only if you add the sp parameter.
    If you use that for main services, you will dynamically create new URLs! This may be dangerous as they will disappear if you stop the server. Be very careful to re-create these URLs when you relaunch the server, otherwise, some external links or bookmarks will be broken!
    The use of that feature is discouraged for coservices without timeout, as such coservices will be available only until the end of the server process (and it is not possible to re-create them with the same key).
  • Do not register twice the same service in the public table, and do not replace a service by a directory (or vice versa). If this happens during the initialisation phase, the server won't start. If this happens after, it will be ignored (with a warning in the logs).
  • All services (but non-attached ones) must be created in a module loaded inside a <site> tag of the config file (because they will be attached to a directory). Not possible for modules loaded inside <extension> or <library>.
  • GET coservices (whithout POST parameters) can be registered only with a main service without GET/POST parameters as fallback. But it may be a preapplied service (see below).
  • Services with POST parameters (main service or coservice) can be registered with a (main or co) service without POST parameters as fallback.
  • The registration of (main) services must be completed before the end of the loading of the module. It not possible to launch a (Lwt) thread that will register a service later, as registering a service needs access to config file information (for example the directory of the site). If you do this, the server will raise Eliom_function_forbidden_outside_site_loading most of the time, but you may also get unexpected results (if the thread is executed while another site is loaded). If you use threads in the initialization phase of your module (for example if you need informations from a database), use Lwt_unix.run to wait the end of the thread.

3. More details on services and page generation

You now know all Eliom's main concepts. In that part, we'll give more details on some aspects that have been seen before:

  • The different types of output for services
  • Timeouts and error handling
  • Persistence of sessions
  • Advanced forms

Static parts

Fully static pages

The staticmod extension allows to associate to your site a static directory where you can put all the static (non generated) parts of your web-site (for examples images ans stylesheets). See the default config file ocsigen.conf to learn how to do that. A predefined service can be used to make links to static files. Get it using (static_dir sp). That service takes as string parameter the name of the file.
For example


Eliom.a 
  (static_dir ~sp)
  sp
  [pcdata "download image"] 
  "ocsigen8-100x30.png"

creates this link: download image

It is now also possible to handle static pages with Eliom, using Eliom.Files (see later).

Other kinds of pages

Sending portions of pages

The Blocks module allows to register services that send portions of pages, of any type that may be contained directly in a <body> tag (blocks of xhtml DTD). It is usefull to create AJAX pages (i.e. pages using the XMLHttpRequest Javascript object). Note that the service returns a list of blocks.



let _ = 
  Blocks.register_new_service 
    ~url:["div"]
    ~get_params:unit
    (fun sp () () -> 
      return 
        [div [h2 [pcdata "Hallo"];
              p [pcdata "Blablablabla"] ]])

The SubXhtml module allows to create other modules for registering portions of pages of other types. For example, Blocks is defined by:



module Blocks = SubXhtml(struct
  type content = Xhtmltypes.body_content
end)

Redirections

The Redirections module allows to register HTTP redirections. If a request is done towards such a service, the server asks the browser to retry with another URL. Example:



let redir = Redirections.register_new_service
    ~url:["redir"]
    ~get_params:(int "o")
   (fun sp o () -> return (make_string_uri coucou_params sp (o,(22,"ee"))))

Try it.

Note that the cost of a redirection is one more query and one more answer.

Sending files

You may want to register a service that will send files. To do that, use the Files module. Example:



let sendfile = 
  Files.register_new_service 
    ~url:["sendfile"]
    ~get_params:unit
    (fun _ () () -> return "filename")

Other example, with suffix URL:



let sendfile2 = 
  Files.register_new_service 
    ~url:["files"]
    ~get_params:(suffix (all_suffix "filename"))
    (fun _ s () -> return ("//path//"^(string_of_url_path s)))

The extension Staticmod is another way to handle static files (see the default configuration file for more informations).

Registering services that decide what they want to send

You may want to register a service that will send, for instance, sometimes an xhtml page, sometimes a file, sometimes something else. To do that, use the Any module, together with the send function of the module you want to use. Example:



let send_any = 
  Any.register_new_service 
    ~url:["sendany"]
    ~get_params:(string "type")
   (fun sp s () -> 
     if s = "valid"
     then
       return
         (Xhtml.send sp
           (html
             (head (title (pcdata "")) [])
             (body [p [pcdata 
                         &quot;This page has been statically typechecked. 
                         If you change the parameter in the URL you 
                         will get an unchecked text page&quot;]])))
     else 
       return
         (HtmlText.send sp 
            &quot;<html><body><p>It is not a valid page. Put 
            type=\""

See a valid page, and a non valid page.

You may also use Any to send cookies or to choose a different charset than the default (default charset is set in configuration file) for the page you send. To do that use the optional parameters ?cookies and ?charset of the send function.

Cookies

A simplest way to set your own cookies on the client is to use functions like Eliom.Xhtml.Cookies.register instead of Eliom.Xhtml.register. The function you register returns a pair containing the page (as usual) and a list of cookies, of type



type cookies = 
  | Set of string list option * float option * (string * string) list
  | Unset of (string list option * string list)

The string option is a the path for which you want to set/unset the cookie (relative to the main directory of your site, defined in the configuration file). None means for all your site.

The float option is a the expiration date (Unix timestamp, in seconds since the epoch). None means that the cookie will expire when the browser will be closed.

You can access the cookies sent by the browser using Eliom.get_cookies sp.

Example:



let cookiename = "mycookie"

let cookies = new_service ["cookies"] unit ()

let _ = Cookies.register cookies
    (fun sp () () -> 
      return
       ((html
         (head (title (pcdata "")) [])
         (body [p [pcdata (try
                             "cookie value: "^
                             (List.assoc cookiename (Eliom.get_cookies sp))
                           with _ -> "<cookie not set>");
                   br ();
                   a cookies sp [pcdata "send other cookie"] ()]])),
        [Extensions.Set (None, None, 
                         [(cookiename,(string_of_int (Random.int 100)))])]))

Try it.

Other concepts

Pre-applied services

Services or coservices with GET parameters can be preapplied to obtain a service without parameters. Example:



let preappl = preapply coucou_params (3,(4,"cinq"))
    

It is not possible to register something on a preapplied service, but you can use them in links or as fallbacks for coservices.

Giving informations to fallbacks

Fallbacks have access to some informations about what succeeded before they were called. Get this information using Eliom.get_exn sp; That function returns a list of exceptions. That list contains Eliom_Link_too_old if the coservice was not found, and Eliom_Session_expired if the session has expired.

It is also possible to tell actions to send informations to the page generated after them. Just place exceptions in the list returned by the action. These exceptions will also be accessible with Eliom.get_exn. Try to replace the lines above (example of session with actions) by:



(************************************************************)
(************ Connection of users, version 4 ****************)
(************************************************************)

  
exception Bad_user

(* -------------------------------------------------------- *)
(* new login box:                                           *)

let login_box sp action =
  Eliom.Xhtml.post_form action sp
    (fun loginname ->
      let l =
        [pcdata "login: "; 
         string_input ~input_type:`Text ~name:loginname ()]
      in
      let exnlist = Eliom.get_exn sp in
      (* If exnlist is not empty, something went wrong
         during an action. We write an error message: *)
      [p (if List.mem Bad_user exnlist
      then (pcdata "Wrong user")::(br ())::l
      else 
        if List.mem Eliom_Session_expired exnlist
        then (pcdata "Session expired")::(br ())::l
        else l)
     ])
    ()


(* -------------------------------------------------------- *)
(* New handler for connect_action (user logs in):           *)

let connect_action_handler sp () login =
  close_session ~sp >>= fun () -> 
  if login = "toto" (* Check user and password :-) *)
  then begin
    Eliom.set_session_data my_table sp login; 
    return []
  end
  else return [Bad_user]



If the actions raises an exception (with Lwt.fail), the server will send an error 500 (like for any other service). Think about catching the exceptions and put them in the list if they correspond to usual cases you want to handle while generating the page after the action.

Disposable coservices

It is possible to set a limit to the number of uses of (attached or non-attached) coservices. Just give the maximum number of uses with the optional ?max_use parameter while creating your coservices. Example



let disposable = new_service ["disposable"] unit ()

let _ = register disposable
    (fun sp () () -> 
      let disp_coservice = 
        new_coservice ~max_use:2 ~fallback:disposable ~get_params:unit ()
      in
      register_for_session sp disp_coservice
        (fun sp () () -> 
          return
            (html
              (head (title (pcdata "")) [])
              (body [p [pcdata "I am a disposable coservice";
                        br ();
                        a disp_coservice sp [pcdata "Try me once again"] ()]]))
        );
      return
        (html
          (head (title (pcdata "")) [])
          (body [p [(if List.mem Eliom.Eliom_Link_too_old (Eliom.get_exn sp)
                    then pcdata &quot;Your link was outdated. I am the fallback. 
                            I just created a new disposable coservice. 
                            You can use it only twice.&quot;
                    else
                    pcdata &quot;I just created a disposable coservice. 
                            You can use it only twice.&quot;);
                    br ();
                    a disp_coservice sp [pcdata "Try it!"] ()]])))

Try it.

Timeout for sessions

The default timeout for sessions in one hour. Sessions will be automatically closed after that amount of time of inactivity from the user. You can change that value for your whole site during initialisation using:



Eliom.set_global_timeout (Some 7200.)

Here 7200 seconds. None means no timeout.

You can change that value for your whole site after initialisation using:



Eliom.set_global_timeout ~sp (Some 7200.)

You can change that value for one user only using:



Eliom.set_user_timeout ~sp (Some 7200.)

Note that there is also a possibility to change the default value for Eliom in the configuration file like this:



    <extension module="//path_to///eliom.cma">
      <timeout value="7200"/>
    </extension>

means no timeout.

Warning: that default may be overriden by each site using Eliom.set_global_timeout. If you want your user to be able to set the default in the configuration file for your site (between <site> and </site>), you must parse the configuration (Eliom.get_config () function, see below).

Timeout for coservices

It is also possible to put timeouts on coservices using the optional parameter ?timeout of functions new_coservice, new_coservice', etc. Note that session coservices cannot survive after the end of the session. Use this if you don't want your coservice to be available during all the session duration. For example if your coservice is here to show the results of a search, you probably want it to be available only for a short time. The following example shows a coservice with timeout registered in the session table.



let timeout = new_service ["timeout"] unit ()

let _ = 
  let page sp () () = 
    let timeoutcoserv =
      register_new_coservice_for_session
        ~sp ~fallback:timeout ~get_params:unit ~timeout:5.
        (fun _ _ _ ->
           return
             (html
               (head (title (pcdata "Coservices with timeouts")) [])
               (body [p 
                 [pcdata "I am a coservice with timeout."; br ();
                  pcdata "I will disappear after 5 seconds of inactivity." ];
                 ])))
    in
    return
      (html
        (head (title (pcdata "Coservices with timeouts")) [])
        (body [p 
          [pcdata "I just created a coservice with 5 seconds timeout."; br ();
           a timeoutcoserv sp [pcdata "Try it"] (); ];
          ]))
  in
  register timeout page

See this example here.

Registering coservices in public table during session

If you want to register coservices in the public table during a session, (that is, after the initialisation phase of your module), you must add the optional sp parameter to the register function. Remember that using register without sp is possible only during initialisation!

We recommend to put a timeout on such coservices, otherwise, they will be available until the end of the server process, and it will not be possible to re-create them when the server is relaunched.

The following example is a translation of the previous one using the public table:



let publiccoservsession = new_service ["publiccoservsession"] unit ()

let _ = 
  let page sp () () = 
    let timeoutcoserv =
      register_new_coservice
        ~sp ~fallback:publiccoservsession ~get_params:unit ~timeout:5.
        (fun _ _ _ ->
           return
             (html
               (head (title (pcdata "Coservices with timeouts")) [])
               (body [p 
                 [pcdata "I am a public coservice with timeout."; br ();
                  pcdata "I will disappear after 5 seconds of inactivity." ];
                 ])))
    in
    return
      (html
        (head (title (pcdata "Public coservices with timeouts")) [])
        (body [p 
          [pcdata &quot;I just created a public coservice 
                   with 5 seconds timeout.&quot;; br ();
           a timeoutcoserv sp [pcdata "Try it"] (); ];
          ]))
  in
  register publiccoservsession page

Define an exception handler for the whole site

When an exception is raised during the generation of a page, or when the page has not been found or has wrong parameters, an HTTP error 500 or 404 is sent to the client. You may want to catch these exceptions to print your own error page. Do this using Eliom.set_exn_handler. Here is the handler used by this tutorial:



let _ = Eliom.set_exn_handler 
   (fun sp e -> match e with
    | Extensions.Ocsigen_404 -> 
       return
         (Xhtml.send ~code:404 ~sp
          (html
            (head (title (pcdata "")) [])
            (body [h1 [pcdata "Eliom tutorial"]; 
                   p [pcdata "Page not found"]])))
    | Eliom_Wrong_parameter ->
       return
         (Xhtml.send ~sp
          (html
            (head (title (pcdata "")) [])
            (body [h1 [pcdata "Eliom tutorial"]; 
                   p [pcdata "Wrong parameters"]])))
    | e -> fail e)

Giving configuration options to your sites

You can add your own options in the configuration file for your Web site. For example:



    <eliom module="//path_to///yourmodule.cmo">
      <youroptions> ...
    </eliom>

Use Eliom.get_config () during the initialization of your module to get the data between <eliom> and </eliom>. Warning: parsing these data is very basic for now. That feature will be improved in the future.

Persistence of sessions

Tables of sessions (for data or services) are kept in memory, and thus will disappear if you close the server process. To solve this problem, Ocsigen allows to reload the modules of your configuration file without shutting down the server. Another solution provided by Eliom is to save session data on hard disk.

Updating sites without shutting down the server

To reload the modules of the configuration file without stoping the server, use /etc/init.d/ocsigen reload for most of the distributions, or do it manually using:


echo reload > /var/run/ocsigen_command

.

Only modules loaded inside <site> or <library> will be reloaded. Module loaded using <extension> will not.

Have a look at the logs to see if all went well during the reload. If something went wrong, old services may still be reachable.

Note that coservices created with the old modules or URLs that have not been masked by new ones will still reachable after the update.

During the reload, some information of the configuration file will not be re-read (for example port numbers, user and group, etc.).

Persistent data

Eliom allows to use more persistent data, using the module Ocsipersist. (Ocsipersist is needed in eliom.cma, thus you need to dynlink it in the configuration file before Eliom). There are currently two implementations of Ocsipersist: ocsipersist-dbm.cma and ocsipersist-sqlite.cma (that depends on sqlite3.cma).

Note that persistent data are serialized on hard disk using OCaml's Marshal module.

  • It is not possible to serialize closures or services
  • Do not modify the type of serialized data, otherwise the server will crash!

Persistent references

Ocsipersist allows to create persistent references. Here is an example of page with a persistent counter:



let mystore = Ocsipersist.open_store "eliomexamplestore"

let count2 = 
  let next =
    let cthr = Ocsipersist.make_persistent mystore "countpage" 0 in
    (fun () -> 
      cthr >>=
      (fun c -> Ocsipersist.get c >>=
        (fun oldc -> 
          let newc = oldc + 1 in
          Ocsipersist.set c newc >>=
          (fun () -> return newc))))
  in
  register_new_service 
    ~url:["count2"]
    ~get_params:unit
    (fun _ () () ->  
      next () >>=
      (fun n ->
        return
         (html
          (head (title (pcdata "counter")) [])
          (body [p [pcdata (string_of_int n)]]))))


See this example here.

Persistent tables

Ocsipersist also allows to create very basic persistent tables. Use them if you don't need complex requests on your tables. Otherwise use a database such as PostgreSQL or MySQL. Here are the interface you can use:



type 'value table

val open_table : string -> 'value table

val find : 'value table -> string -> 'value Lwt.t

val add : 'value table -> string -> 'value -> unit Lwt.t

val remove : 'value table -> string -> unit Lwt.t

As you can see, all these function are cooperative.

Persistent session data

Eliom also implements persistent session tables. You can use them instead of memory tables if you don't need to register closures.

The following example is a new version of our site with users, with persistent connections. (login_box, disconnect_box and disconnect_action are the same as before).



(************************************************************)
(************ Connection of users, version 5 ****************)
(**************** (persistent sessions) *********************)
(************************************************************)


let my_persistent_table = 
  create_persistent_table "eliom_example_table"

(* -------------------------------------------------------- *)
(* We create one main service and two (POST) actions        *)
(* (for connection and disconnection)                       *)

let persist_session_example = 
  Eliom.new_service
    ~url:["persist"] 
    ~get_params:unit 
    ()

let persist_session_connect_action = 
  Eliom.new_post_coservice'
    ~post_params:(string "login") 
    ()

(* disconnect_action, login_box and disconnect_box have been
   defined in the section about actions *)


(* ----------------------------------------------------------- *)
(* Handler for "persist_session_example" service (main page):  *)

let persist_session_example_handler sp () () = 
  Eliom.get_persistent_data my_persistent_table sp >>= fun sessdat ->
  return
    (html
       (head (title (pcdata "")) [])
       (body 
          (match sessdat with
          | Some name ->
              [p [pcdata ("Hello "^name); br ()];
              disconnect_box sp "Close session"]
          | None -> [login_box sp persist_session_connect_action;
                     p [em [pcdata "The only user is 'toto'."]]]
          )))


(* ----------------------------------------------------------- *)
(* Handler for persist_session_connect_action (user logs in):  *)

let persist_session_connect_action_handler sp () login =
  close_session ~sp >>= fun () -> 
  if login = "toto" (* Check user and password :-) *)
  then begin
    Eliom.set_persistent_data my_persistent_table sp login >>= fun () ->
    return []
  end
  else return [Bad_user]


(* -------------------------------------------------------- *)
(* Registration of main services:                           *)

let () = 
  Eliom.Xhtml.register 
    ~service:persist_session_example
    persist_session_example_handler;
  Actions.register 
    ~service:persist_session_connect_action 
    persist_session_connect_action_handler

See this example here.

As it is not possible to serialize closures, there is no persistent session service table. Be very carefull if you use both persistent session data tables and service session tables, as your session may become inconsistent (use the session service table only for volatile services, like coservices with timeouts).

Advanced forms

This section shows more advanced use of page parameters and corresponding forms.

Parse parameters using regular expressions

Eliom.regexp allows to parse page parameters using (Perl-compatible) regular expressions. We use the module Netstring_pcre, from OCamlnet. See the documentation about OCamlnet for more informations. The following example shows a service that accepts only parameters values enclosed between [ and ]:



let r = Netstring_pcre.regexp "\\\\[(.*)\\\\]"

let regexp = 
  Eliom.Xhtml.register_new_service 
    ~url:["regexp"]
    ~get_params:(regexp r "$1" "myparam")
    (fun _ g () -> 
      return 
        (html
           (head (title (pcdata "")) [])
           (body [p [pcdata g]])))





Try it.

Boolean checkboxes

Page may take parameter of type bool. A possible use of this type is in a form with boolean checkboxes, as in the example below:



(* Form with bool checkbox: *)
let bool_params = register_new_service 
    ~url:["bool"]
    ~get_params:(bool "case")
  (fun _ case () -> 
    return
    << <html>
         <head><title></title></head>
         <body>
         <p>
           $pcdata (if case then "checked" else "not checked")$
         </p>
         </body>
       </html> >>)

let create_form_bool casename =
    <:xmllist< <p>check? $bool_checkbox ~name:casename ()$ <br/>
      $string_input ~input_type:`Submit ~value:"Click" ()$</p> >>

let form_bool = register_new_service ["formbool"] unit
  (fun sp () () -> 
     let f = get_form bool_params sp create_form_bool in 
     return
     << <html>
          <head><title></title></head>
          <body> $f$ </body>
        </html> >>)



Try it.

Important warning: As you can see, browsers do not send any value for unchecked boxes! An unchecked box is equivalent to no parameter at all! Thus it is not possible to distinguish between a service taking a boolean and a service taking no parameter at all (if they share the same URL). In Eliom services are tried in order of registration! The first matching service will answer.

Other types similar to bool:

  • opt (page taking an optional parameter),
  • sum (either a parameter or another).

See the interface here.

Type set

Page may take several parameters of the same name. It is usefull when you want to create a form with a variable number of fields. To do that with Eliom, use the type set. For example set int "val" means that the page will take zero, one or several parameters of name "val", all of type int. The function you register will receive the parameters in a list. Example:




let set = register_new_service 
    ~url:["set"]
    ~get_params:(set string "s")
  (fun _ l () ->
    let ll = 
      List.map 
        (fun s -> << <strong>$str:s$ </strong> >>) l 
    in  
    return
    << <html>
         <head><title></title></head>
         <body>
         <p>
           You sent: 
           $list:ll$
         </p>
         </body>
       </html> >>)

These parameters may come from several kinds of widgets in forms. Here is an example of a form with several checkboxes, all sharing the same name, but with different values:




(* form to set *)
let setform = register_new_service 
    ~url:["setform"]
    ~get_params:unit
    (fun sp () () ->
      return
        (html
           (head (title (pcdata "")) [])
           (body [h1 [pcdata "Set Form"];
                  get_form set sp 
                    (fun n ->
                      [p [pcdata "Form to set: ";
                          string_checkbox ~name:n ~value:"box1" ();
                          string_checkbox 
                            ~name:n ~value:"box2" ~checked:true ();
                          string_checkbox ~name:n ~value:"box3" ();
                          string_checkbox ~name:n ~value:"box4" ();
                          string_input ~input_type:`Submit ~value:"Click" ()]])
                ])))

Try it.

Once again, note that there is no difference between an empty set or no parameter at all. If you register a service without parameters and a service with a set of parameters on the same URL, the firstly registered service that matches will answer.

Clickable images

Here is an example of clickable image. You receive the coordinates the user clicked on.



let coord = register_new_service 
    ~url:["coord"]
    ~get_params:(coordinates "coord")
  (fun _ c () ->
    return
  << <html>
       <head><title></title></head>
       <body>
       <p>
         You clicked on coordinates: 
         ($str:(string_of_int c.abscissa)$, $str:(string_of_int c.ordinate)$)
       </p>
       </body>
     </html> >>)

(* form to image *)
let imageform = register_new_service 
    ~url:["imageform"]
    ~get_params:unit
    (fun sp () () ->
      return
        (html
           (head (title (pcdata "")) [])
           (body [h1 [pcdata "Image Form"];
                  get_form coord sp 
                    (fun n ->
                      [p [image_input 
                            ~src:(make_uri (static_dir sp) sp ["ocsigen5.png"])
                            ~name:n
                            ()]])
                ])))

Try it.

You may also send a value with the coordinates:



let coord2 = register_new_service 
    ~url:["coord2"]
    ~get_params:(int_coordinates "coord")
  (fun _ (i, c) () ->
    return
  << <html>
       <head><title></title></head>
       <body>
       <p>
         You clicked on coordinates: 
         ($str:(string_of_int c.abscissa)$, $str:(string_of_int c.ordinate)$)
       </p>
       </body>
     </html> >>)

(* form to image *)
let imageform2 = register_new_service 
    ~url:["imageform2"]
    ~get_params:unit
    (fun sp () () ->
      return
        (html
           (head (title (pcdata "")) [])
           (body [h1 [pcdata "Image Form"];
                  get_form coord2 sp 
                    (fun n ->
                      [p [int_image_input 
                            ~src:(make_uri (static_dir sp) sp ["ocsigen5.png"])
                            ~name:n
                            ~value:3
                            ()]])
                ])))


Try it.

Type list

Another way (than set) to do variable length forms is to use indexed lists. The use of that feature is a bit more complex than set and still experimental. Here is an example of service taking an indexed list as parameter:




(* lists *)
let coucou_list = register_new_service 
    ~url:["coucou"]
    ~get_params:(list "a" (string "str"))
  (fun _ l () ->
    let ll = 
      List.map (fun s -> << <strong>$str:s$</strong> >>) l in 
    return
      << <html>
           <head><title></title></head>
           <body>
           <p>
             You sent: 
             $list:ll$
           </p>
           </body>
         </html> >>)

Here is an example of link towards this service: coucou?a.str[0]=toto&a.str[1]=titi.

Warning: As for sets or bools, if a request has no parameter, it will be considered as the empty list. Services are tried in order of registration.

As you see, the names of each list element is built from the name of the list, the name of the list element, and an index. To spare you creating yourself these names, Eliom provides you an iterator to create them.





(* Form with list: *)
let create_listform f = 
  (* Here, f.it is an iterator like List.map, 
     but it must be applied to a function taking 2 arguments 
     (unlike 1 in map), the first one being the name of the parameter,
     and the second one the element of list.
     The last parameter of f.it is the code that must be appended at the 
     end of the list created
   *)
  f.it (fun stringname v ->
    <:xmllist< <p>Write the value for $str:v$: 
      $string_input ~input_type:`Text ~name:stringname ()$ </p> >>)
    ["one";"two";"three";"four"]
    <:xmllist< <p>$string_input ~input_type:`Submit ~value:"Click" ()$</p> >>

let listform = register_new_service ["listform"] unit
  (fun sp () () -> 
     let f = get_form coucou_list sp create_listform in 
     return
      << <html>
           <head><title></title></head>
           <body> $f$ </body>
         </html> >>)


Try it.

Important warning: As we have seen in the section about boolean (or optional) parameters, it is not possible to distinguish between a boolean with value "false", and no parameter at all. This causes problems if you create a list of boolean or optional values, as it is not possible to know the length of the list. In that case, Eliom always takes the shortest possible list.

Forms and suffixes

Service with "suffix" URLs have an equivalent version with usual parameters, allowing to create forms towards such services. Example:



(* Form for service with suffix: *)
let create_suffixform ((suff, endsuff),i) =
    <:xmllist< <p>Write the suffix: 
      $int_input ~input_type:`Text ~name:suff ()$ <br/>
      Write a string: $user_type_input 
         ~input_type:`Text ~name:endsuff string_of_url_path$ <br/>
      Write an int: $int_input ~input_type:`Text ~name:i ()$ <br/>
      $string_input ~input_type:`Submit ~value:"Click" ()$</p> >>

let suffixform = register_new_service ["suffixform"] unit
  (fun sp () () -> 
     let f = get_form isuffix sp create_suffixform in 
     return
      << <html>
           <head><title></title></head>
           <body> $f$ </body>
         </html> >>)


Try it.

Uploading files

The file parameter type allows to send files in your request. The service gets something of type Extensions.file_info. You can extract informations from this using:



val get_tmp_filename : Extensions.file_info -> string
val get_filesize : Extensions.file_info -> int64
val get_original_filename : Extensions.file_info -> string

Eliom.get_tmp_filename allows to know the actual name of the uploaded file on the hard disk. Eliom.get_original_filename gives the original filename.

To make possible the upload of files, you must configure a directory for uploaded files in Ocsigen's configuration file. For example:



  <uploaddir>/tmp</uploaddir>

Files are kept in this directory only during the request. Then they are automatically cancelled. Thus your services must copy them somewhere else themselves if they want to keep them. In the following example, we create a new hard link to the file to keep it (the destination must be on the same partition of the disk).



let upload = new_service
    ~url:["upload"]
    ~get_params:unit
    ()
    
let upload2 = register_new_post_service
   ~fallback:upload
   ~post_params:(file "file")
    (fun _ () file ->
      let to_display = 
        let newname = "/tmp/thefile" in
        (try
          Unix.unlink newname;
        with _ -> ());
        Unix.link (Eliom.get_tmp_filename file) newname;
        let fd_in = open_in newname in
        try
          let line = input_line fd_in in close_in fd_in; line (*end*)
        with End_of_file -> close_in fd_in; "vide"
      in
      return
        (html
           (head (title (pcdata "Upload")) [])
           (body [h1 [pcdata to_display]])))
    
    
let uploadform = register upload
    (fun sp () () ->
      let f =
        (post_form upload2 sp
           (fun file ->
             [p [file_input ~name:file ();
                 br ();
                 string_input ~input_type:`Submit ~value:"Send" ()
               ]]) ()) in 
      return
        (html
           (head (title (pcdata "form")) [])
           (body [f])))
                                                                         


Try it (warning: uploading on ocsigen.org is forbidden).

Predefined constructs

Images, CSS, Javascript

To include an image, use simply the function XHTML.M.img:


img ~alt:"Ocsigen" 
    ~src:(make_uri (static_dir sp) sp ["ocsigen1024.jpg"])
    ()

The function make_uri creates the relative URL string from current URL (in sp) (see above) to the URL of the image in the static directory configured in the configuration file.

To simplify the creation of <link> tags for CSS or <script> tags for Javascript, use the following functions:


css_link (make_uri (static_dir sp) sp ["style.css"])


js_script (make_uri (static_dir sp) sp ["funs.js"])

Menus

To make a menu on your web page, you can use the function Eliomboxes.menu. First, define your menu like this:


let mymenu current sp =
  Eliomboxes.menu ~classe:["menuprincipal"]
    (home, <:xmllist< Home >>)
    [
     (infos, <:xmllist< More info >>);
     (tutorial, <:xmllist< Documentation >>)
   ] current sp

Here, home, infos, and tutorial are your three pages (generated for example by Eliom.new_service).

Then mymenu home sp will generate the following code:

<ul class="menu menuprincipal"> <li class="current first">Home </li> <li><a href="infos">More info</a> </li> <li class="last"><a href="tutorial">Documentation</a> </li> </ul>

Personalise it in your CSS style-sheet.

Eliomboxes.menu takes a list of services without GET parameters. If you want one of the link to contains GET parameters, pre-apply the service.

How to make a menu entry with GET parameters?

Preapply your service.

Examples

Write a forum

As an example, we will now write a small forum. Our forum has a main page, summarising all the messages and a page for each message. All the functions to access the database and print the result are left to the reader. We only want to show the structure of the site. Suppose you have written a function news_headers_list_box that writes the beginning of messages, and message_box that write a full message.



(* All the services: *)

let main_page = new_service ~url:[""]
    ~get_params:unit ()

let news_page = new_service ["msg"] (int "num") ()

(* Construction of pages *)

let home sp () () =
  page sp
    [h1 [pcdata "Mon site"];
     news_headers_list_box 
       sp anonymoususer news_page]

let print_news_page sp i () = 
  page sp
    [h1 [pcdata "Info"];
     message_box i anonymoususer]

(* Services registration *)

let _ = register
  ~service:main_page
  home

let _ = register
  ~service:news_page
  print_news_page

Now the same example with a login box on each page. We now have two versions of each page: connected and not connected. We need two actions (for connection and disconnection). Suppose we have the functions login_box, connected_box, and connect.


(* All the services: *)

let main_page = new_service ~url:[""] ~get_params:unit ()

let news_page = new_service ["msg"] (int "num") ()

let connect_action =
  new_post_coservice'
    ~post_params:(string "login" ** string "password")

(* Construction of pages *)

let home sp () () =
   match get_session_data my_table sp with
   | None ->
     page sp
       [h1 [pcdata "Mon site"];
        p [pcdata "(user : toto and password : titi)"];
        login_box sp connect_action;
        news_headers_list_box sp anonymoususer news_page]
   | Some user ->
      page sp
        [h1 [pcdata "Mon site"];
         text_box "Bonjour !";
         connected_box sp user disconnect_action;
         news_headers_list_box sp user news_page]

let print_news_page sp i () = 
   match get_session_data my_table sp with
   | None ->
      page sp
        [h1 [pcdata "Info"];
         login_box sp connect_action;
         message_box i anonymoususer]
   | Some user ->
      page sp
        [h1 [pcdata "Info"];
         connected_box sp user disconnect_action;
         message_box i user]

(* Services registration *)

let _ = register
  ~service:main_page
  home

let _ = register
  ~service:news_page
  print_news_page

let launch_session sp user =
  set_session_data my_table sp user

let _ = Actions.register
  ~action:connect_action
    (fun h (login, password) ->
      launch_session sp (connect login password); return [])

Nurpawiki

Ocsigen's source code contains an example of Wiki written with Eliom by Janne Hellsten. It is called Nurpawiki.

.

Ocsigen

This page has been generated by Ocsimore. If you are a member of the Ocsigen team, you can log in to modify the pages.