A quick tutorial on how to create a mini web application with Fantom's Wisp.

A guide to serving web pages, posting forms, and uploading files.

Using Wisp is Fantom web programming at its most basic level. You have to do everything manually yourself from URL routing, through to setting the return Content-Type HTTP header.

It is the bare bones entry level API that is accessible to everyone, and as such, is useful to know before you move on to more powerful frameworks such as BedSheet and Pillow.

wisp and web are part of the core Fantom libraries and as such are bundled with SkySpark. So this tutorial may also interest SkySpark developers writing extensions.

For reference this tutorial was written with Fantom 1.0.69 and assumes the reader is already able to set up a Fantom project, build .pod files, and run code.

Contents

Starting Wisp

Note that SkySpark developers don't need to worry about starting Wisp, as SkySpark obviously does that for us.

Wisp is Fantom's web server. To start it we're going to have our Main class extend util::AbstractMain so we can use its runServices() method.

When creating a WispService we need to pass in an instance of a WebMod. It is the WebMods responsibility to serve up web content. For now, we're just going to have every GET request return the text Hello Mum! . We do this by overriding the onGet() method.

select all

using util::AbstractMain using wisp::WispService using web::WebMod class Main : AbstractMain { override Int run() { runServices([WispService { it.httpPort = 8069 it.root = MyWebMod() }]) } } const class MyWebMod : WebMod { override Void onGet() { res.headers["Content-Type"] = "text/plain" res.out.print("Hello Mum!") } }

Running the program and pointing a browser at http://localhost:8069/ then gives us:

2. Serving Web Pages

As great as plain text is, we need to serve up HTML pages. This can be accomplished by changing the returned Content-Type header to text/html .

We also need the ability to serve different pages from different URLs. A simple switch statement on the URL path offers basic routing and the ability to send 404 responses.

Our MyWebMod class now looks like:

select all

const class MyWebMod : WebMod { override Void onGet() { url := req.modRel.pathOnly switch (url) { case `/`: onGetIndexPage() default: res.sendErr(404) } } Void onGetIndexPage() { res.headers["Content-Type"] = "text/html" res.out.print("<!DOCTYPE html> <html> <head> <title>Wisp Example</title> </head> <body> <h1>Hello Mum!</h1> </body> </html>") } }

Which serves up a page like this:

3. Posting Forms

Now lets post data to the server. To do that, we're going to change the index page html to incorporate a form:

select all

<!DOCTYPE html> <html> <head> <title>Post Form Example</title> </head> <body> <h1>Post Form</h1> <form method='POST' action='/postForm'> <label for='name'>Name</label> <input type='text' name='name'> <br/> <label for='beer'>Beer</label> <input type='text' name='beer'> <br/> <input type='submit' /> </form> </body> </html>

Note the method attribute in the HTML form, method="POST" . This means the form will be posted to the server. If it were GET then the form values would be sent up as a query string.

Also note the action attribute in the HTML form, action="/postForm" . This is the URL that the form will be posted to, and need to be handled on the server.

Our page now looks like:

To handle a POST to /postForm we need to override the onPost() method. We'll write another switch statement, just as we did for onGet() :

select all

override Void onPost() { url := req.modRel.pathOnly switch (url) { case `/postForm`: onPostForm() // <-- need to implement this method default: res.sendErr(404) } }

Now we'll implement onPostForm() and have it print out our form values. To access the form values, use WebRes.form() which is just a handy string map of name / value pairs. Note that all submitted form values are of type Str .

select all

Void onPostForm() { name := req.form["name"] beer := req.form["beer"] res.headers["Content-Type"] = "text/html" res.out.print("<!DOCTYPE html> <html> <head> <title>Post Form Values</title> </head> <body> <p>Hello <b>${name}</b>, you like <b>${beer}</b>!</p> </body> </html>") }

If we submit our form, we should now see:

4. Uploading Files

As we saw, posting forms was easy. But how about uploading files? In other languages, such as Java, this can be notoriously tricky. But as you'll see, here in Fantom land it's still pretty simple.

First we need to add a file input to our form.

And note that for file uploads it is mandatory that we change form encoding by adding the attribute, enctype="multipart/form-data" .

select all

<!DOCTYPE html> <html> <head> <title>Post Form Example</title> </head> <body> <h1>Post Form</h1> <form method='POST' action='/postForm' enctype='multipart/form-data'> <label for='name'>Name</label> <input type='text' name='name'> <br/> <label for='beer'>Beer</label> <input type='text' name='beer'> <br/> <label for='photo'>Photo</label> <input type='file' name='photo'> <br/> <input type='submit' /> </form> </body> </html>

Which renders:

Handling form content with uploaded files on the server is a little more complicated due to the encoding. We can no longer use the handy WebReq.form() map, instead we have to use WebReq.parseMultiPartForm() . It takes a function which is invoked for every input in the form, both file uploads and normal values. The function gives us the input name, a stream of raw data, and any meta associated with the input.

Our onPostForm() now looks like:

select all

Void onPostForm() { name := null as Str beer := null as Str photoName := null as Str photoBuf := null as Buf req.parseMultiPartForm |Str inputName, InStream in, Str:Str headers| { if (inputName == "name") name = in.readAllStr if (inputName == "beer") beer = in.readAllStr if (inputName == "photo") { quoted := headers["Content-Disposition"]?.split(';')?.find { it.lower.startsWith("filename") }?.split('=')?.getSafe(1) photoName = quoted == null ? null : WebUtil.fromQuotedStr(quoted) photoBuf = in.readAllBuf } } res.headers["Content-Type"] = "text/html" res.out.print("<!DOCTYPE html> <html> <head> <title>Post Form Values</title> </head> <body> <p> Hello <b>${name}</b>, you like <b>${beer}</b>! The photo <b>${photoName}</b> looks like: </p> <img src='data:${photoName.toUri.mimeType};base64,${photoBuf.toBase64}'> </body> </html>") }

We read the raw file data in as a Buf , and then spit it out as in inline Base64 encoded image. But you could do whatever you like with it; read CSV values, save it to a database...

The long line of code that grabs the quoted string:

quoted := headers["Content-Disposition"]?.split(';')?.find { it.lower.startsWith("filename") }?.split('=')?.getSafe(1)

is a means to parse the Content-Disposition header, which looks like this:

Content-Disposition: form-data; name="photo"; filename="beer.png"

As you can see, it contains the name of the uploaded file which can be very handy. Note that browsers typically only send up a file name and not the entire path. This is a security consideration so servers don't learn anything of the client computer.

The net result is that we end up with this web page:

Tidy Up - Use WebOutStream

Not every one is a fan of long, multi-line strings for rendering HTML. An alternative is to use the print methods on the WebOutStream. It's a means to print HTML in a more programmatic way without having to worry about leading tabs and spaces.

Here is our onGetIndexPage() method refactored to make use of WebOutStream :

select all

Void onGetIndexPage() { res.headers["Content-Type"] = "text/html" out := res.out out.docType5 out.html out.head out.title.w("Post Form Example").titleEnd out.headEnd out.body out.h1.w("Post Form").h1End out.form("method='POST' action='/postForm' enctype='multipart/form-data'") out.label("for='name'").w("Name").labelEnd out.input("type='text' name='name'") out.br out.label("for='beer'").w("Beer").labelEnd out.input("type='text' name='beer'") out.br out.label("for='photo'").w("Photo").labelEnd out.input("type='file' name='photo'") out.br out.submit out.formEnd out.bodyEnd out.htmlEnd }

I would say that using WebOutStream is no better or worse than multi-line strings, just different.

6. Complete Example

Below is the complete example that:

Starts the Wisp web server

Has basic routing for page URLs

Displays a HTML index page, complete with a file upload form

Has basic routing for form posts

Parses uploaded form data

Displays the data in a new HTML page

Uses WebOutStream

select all

using util::AbstractMain using wisp::WispService using web::WebMod using web::WebUtil class MainWisp : AbstractMain { override Int run() { runServices([WispService { it.httpPort = 8069 it.root = MyWebMod() }]) } } const class MyWebMod : WebMod { override Void onGet() { url := req.modRel.pathOnly switch (url) { case `/`: onGetIndexPage() default: res.sendErr(404) } } override Void onPost() { url := req.modRel.pathOnly switch (url) { case `/postForm`: onPostForm() default: res.sendErr(404) } } Void onGetIndexPage() { res.headers["Content-Type"] = "text/html" out := res.out out.docType5 out.html out.head out.title.w("Post Form Example").titleEnd out.headEnd out.body out.h1.w("Post Form").h1End out.form("method='POST' action='/postForm' enctype='multipart/form-data'") out.label("for='name'").w("Name").labelEnd out.input("type='text' name='name'") out.br out.label("for='beer'").w("Beer").labelEnd out.input("type='text' name='beer'") out.br out.label("for='photo'").w("Photo").labelEnd out.input("type='file' name='photo'") out.br out.submit out.formEnd out.bodyEnd out.htmlEnd } Void onPostForm() { name := null as Str beer := null as Str photoName := null as Str photoBuf := null as Buf req.parseMultiPartForm |Str inputName, InStream in, Str:Str headers| { if (inputName == "name") name = in.readAllStr if (inputName == "beer") beer = in.readAllStr if (inputName == "photo") { quoted := headers["Content-Disposition"]?.split(';')?.find { it.lower.startsWith("filename") }?.split('=')?.getSafe(1) photoName = quoted == null ? null : WebUtil.fromQuotedStr(quoted) photoBuf = in.readAllBuf } } res.headers["Content-Type"] = "text/html" out := res.out out.docType5 out.html out.head out.title.w("Post Form Values").titleEnd out.headEnd out.body out.p out.w("Hello ").b.w(name).bEnd.w(", you like ").b.w(beer).bEnd.w("! ") out.w("The photo ").b.w(photoName).bEnd.w(" looks like:") out.pEnd out.img(`data:${photoName.toUri.mimeType};base64,${photoBuf.toBase64}`) out.bodyEnd out.htmlEnd } }

Have fun!

Edits