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
- Serving Web Pages
- Posting Forms
- Uploading Files
- Tidy Up - Use WebOutStream
- Complete Example
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.
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:
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:
<!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()
:
override Void onPost() { url := req.modRel.pathOnly switch (url) { case `/postForm`: onPostForm()// <-- need to implement this methoddefault: 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
.
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"
.
<!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:
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
:
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
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
- 30 Dec 2016 - Original article.