racket/collects/web-server/scribblings/tutorial/web-server-tutorial.scrbl
2008-08-18 22:41:50 +00:00

1272 lines
48 KiB
Racket

#lang scribble/doc
@(require scribble/manual
(for-label scheme)
(for-label web-server/servlet)
"tutorial-util.ss")
@title{@bold{Continue}: Web Applications in PLT Scheme}
@author[(author+email "Danny Yoo" "dyoo@cs.wpi.edu")
(author+email "Jay McCarthy" "jay@cs.byu.edu")]
How do we make dynamic web applications? This tutorial will show how we
can build web applications using PLT Scheme. As our working example,
we'll build a simple web journal (a ``blog''). We'll cover how to start
up a web server, how to generate dynamic web content, and how to
interact with the user.
The target audience for this tutorial are students who've gone through
the design and use of structures in @link["http://htdp.org/"]{How to Design Programs}, with some
higher-order functions, @scheme[local], and a minor bit of mutation.
@section{Getting Started}
Everything you needed in this tutorial is provided in @link["http://plt-scheme.org/"]{PLT Scheme}.
We will be using the DrScheme Module language. Enter the following into the Definition window.
@schememod[
web-server/insta
(define (start request)
'(html
(head (title "My Blog"))
(body (h1 "Under construction"))))
]
Press the @onscreen{Run} button. If a web browser comes up with an ``Under
Construction'' page, then clap your hands with delight: you've built
your first web application! It doesn't do much yet, but we will get
there. Press the @onscreen{Stop} button to shut the server down for now.
@section{The Application}
We want to motivate this tutorial by showing how to develop a blog.
Users should be able to create posts and add comments to
any posts. We'll take an iterative approach, with one or two pitfalls
along the way. The game plan, roughly, will be:
@itemize[
@item{Show a static list of posts.}
@item{Allow a user to add new posts to the system.}
@item{Extend the model to let a user add comments to a post.}
@item{Allow all users to share the same set of posts.}
@item{Serialize our data structures to disk.}
]
By the end of this tutorial, we'll have a simple blogging application.
@section{Basic Blog}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-1)]
We start by considering our data definitions. We want to represent a
list of posts. Let's say that a post is:
@schemeblock[(define-struct post (title body))]
@(defstruct post ([title string?] [body string?]))
@bold{Exercise.} Make a few examples of posts.
A blog, then, will be a list of posts:
@(defthing blog (listof post?))
As a very simple example of a blog:
@schemeblock[
(define BLOG (list (make-post "First Post!"
"Hey, this is my first post!")))
]
Now that we have a sample blog structure, let's get our web
application to show it.
@section{Rendering HTML}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-1)]
When a web browser visits our application's URL, the browser
constructs a request structure and sends it off to our web
application. Our start function will consume requests and produce
responses. One basic kind of response is to show an HTML page.
@schemeblock[
(define html-response/c
(or/c string?
(or/c (cons/c symbol? (listof html-response/c))
(cons/c symbol?
(cons/c (listof (list/c symbol? string?))
(listof html-response/c))))))]
For example:
The HTML @tt{hello} is represented as @scheme["hello"].
@tt{<p>This is an example</p>} is
@scheme['(p "This is an example")].
@tt{<a href="link.html">Past</a>} is
@scheme['(a ((href "link.html")) "Past")].
@tt{<p>This is <div class="emph">another</div> example.</p>} is
@scheme['(p "This is " (div ((class "emph")) "another") " example.")].
We can produce these @scheme[html-response]s by using @scheme[cons] and @scheme[list] directly.
Doing so, however, can be notationally heavy. Consider:
@schemeblock[
(list 'html (list 'head (list 'title "Some title"))
(list 'body (list 'p "This is a simple static page.")))
]
vs:
@schemeblock[
'(html (head (title "Some title"))
(body (p "This is a simple static page.")))
]
They both produce the same @scheme[html-response], but the latter is a lot
easier to type and read. We've been using the extended list
abbreviation form described in @link["http://htdp.org/2003-09-26/Book/curriculum-Z-H-17.html#node_chap_13"]{Section 13} of @link["http://htdp.org/"]{How to Design Programs}:
by using a leading forward quote mark to concisely represent the list
structure, we can construct static html responses with aplomb.
However, we can run into a problem when we use simple list
abbreviation with dynamic content. If we have expressions to inject
into the @scheme[html-response] structure, we can't use a simple list-abbreviation
approach because those expressions will be treated literally as part
of the list structure!
We want a notation that gives us the convenience of quoted list
abbreviations, but with the option to treat a portion of the structure
as a normal expression. That is, we would like to define a template
whose placeholders can be easily expressed and filled in dynamically.
Scheme provides this templating functionality with quasiquotation.
Quasiquotation uses a leading back-quote in front of the whole
structure. Like regular quoted list abbreviation, the majority of the
list structure will be literally preserved in the nested list result.
In places where we'd like a subexpression's value to be plugged in, we
prepend an unquoting comma in front of the subexpression. As an
example:
@schemeblock[
@code:comment{render-greeting: string -> html-response}
@code:comment{Consumes a name, and produces a dynamic html-response.}
(define (render-greeting a-name)
`(html (head (title "Welcome"))
(body (p ,(string-append "Hello " a-name)))))
]
@bold{Exercise.} Write a function that consumes a @scheme[post] and produces
an @scheme[html-response] representing that content.
@defthing[render-post (post? . -> . html-response/c)]
As an example, we want:
@schemeblock[
(render-post (make-post "First post!" "This is a first post."))
]
to produce:
@schemeblock[
'(div ((class "post")) "First post!" (p "This is a first post."))
]
@bold{Exercise.} Revise @scheme[render-post] to show the number of comments attached
to a post.
@centerline{------------}
If an expression produces a list of @scheme[html-response] fragments, we may
want to splice in the elements of a list into our template, rather
plug in the whole list itself. In these situations, we can use the
splicing form @scheme[,@expression].
As an example, we may want a helper function that transforms a
@scheme[html-response] list into a fragment representing an unordered, itemized
HTML list:
@schemeblock[
@code:comment{render-as-itemized-list: (listof html-response) -> html-response}
@code:comment{Consumes a list of items, and produces a rendering}
@code:comment{as an unordered list.}
(define (render-as-itemized-list fragments)
`(ul ,@(map render-as-item fragments)))
@code:comment{render-as-item: html-response -> html-response}
@code:comment{Consumes an html-response, and produces a rendering}
@code:comment{as a list item.}
(define (render-as-item a-fragment)
`(li ,a-fragment))
]
@bold{Exercise.} Write a function @scheme[render-posts] that consumes a @scheme[(listof post?)]
and produces an @scheme[html-response] for that content.
@defthing[render-posts ((listof post?) . -> . html-response/c)]
As examples:
@schemeblock[
(render-posts empty)
]
should produce:
@schemeblock[
'(div ((class "posts")))
]
@schemeblock[
(render-posts (list (make-post "Post 1" "Body 1")
(make-post "Post 2" "Body 2")))
]
should produce:
@schemeblock[
'(div ((class "posts"))
(div ((class "post")) "Post 1" "Body 1")
(div ((class "post")) "Post 2" "Body 2"))
]
@centerline{------------}
Now that we have the @scheme[render-posts] function handy, let's revisit our
web application and change our @scheme[start] function to return an interesting
@scheme[html-response].
@external-file["iteration-1.ss"]
If we press Run, we should see the blog posts in our web browser.
@section{Inspecting Requests}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-2
web-server/servlet)]
Our application still seems a bit static: although we're building the
page dynamically, we haven't yet provided a way for an external user
to add new posts. Let's tackle that now. Let's provide a form that
will let the user add a new blog entry. When the user presses the
submit button, we want the user to see the new post at the top of the
page.
Until now, we've been passing around a @scheme[request] object without doing
anything with it. As we might expect, the @scheme[request] object isn't meant
to be ignored so much! When a user fills out a web form and submits
it, that user's browser constructs a new @scheme[request] that holds the form
values in it. We can use the function @scheme[request-bindings] to grab at
the values that the user has filled out. The type of @scheme[request-bindings]
is:
@defthing[request-bindings (request? . -> . bindings?)]
Along with @scheme[request-bindings], there's another function called
@scheme[extract-binding/single] that takes this as well as a name, and returns
the value associated to that name.
@defthing[extract-binding/single (symbol? bindings? . -> . string?)]
Finally, we can check to see if a name exists in a binding with
@scheme[exists-binding?]:
@defthing[exists-binding? (symbol? bindings? . -> . boolean?)]
With these functions, we can design functions that consume @scheme[request]s
and do something useful.
@bold{Exercise.} Write a function @scheme[can-parse-post?] that consumes a @scheme[bindings?].
It should produce @scheme[#t] if there exist bindings both for the symbols
@scheme['title] and @scheme['body], and @scheme[#f] otherwise.
@defthing[can-parse-post? (bindings? . -> . boolean?)]
@bold{Exercise.} Write a function @scheme[parse-post] that consumes a bindings.
Assume that the bindings structure has values for the symbols @scheme['title]
and @scheme['body]. @scheme[parse-post] should produce a post containing those values.
@defthing[parse-post (bindings? . -> . post?)]
Now that we have these helper functions, we can extend our web
application to handle form input. We'll add a small form at the
bottom, and adjust out program to handle the addition of new posts.
Our start method, then, will first see if the request has a parsable
post, extend its set of posts if it can, and finally display those
blog posts.
@external-file["iteration-2.ss"]
This appears to work... but there's an issue with this! Try to add
two new posts. What happens?
@section{Advanced Control Flow}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-3)]
For the moment, let's ignore the admittedly huge problem of having a
blog that only accepts one new blog entry. Don't worry! We will fix
this.
But there's a higher-level problem with our program: although we do
have a function, @scheme[start], that can respond to requests directed at our
application's URL, that @scheme[start] function is starting to get overloaded
with a lot of responsibility. Conceptually, @scheme[start] is now handling
two different kinds of requests: it's either a request for showing a
blog, or a request for adding a new blog post.
What's happening is that @scheme[start] is becoming a traffic cop --- a
dispatcher --- for all the behavior of our web application. As far as
we know, if we want to add more functionality to our application,
start needs to know how to deal. Can we can get different kinds of
requests to automatically direct themselves to different functions?
The web server library provides a function, @scheme[send/suspend/dispatch],
that allows us to create URLs that direct to different parts of our
application. Let's demonstrate a dizzying example. In a new file,
enter the following in the definition window.
@schememod[
web-server/insta
@code:comment{start: request -> html-response}
(define (start request)
(phase-1 request))
@code:comment{phase-1: request -> html-response}
(define (phase-1 request)
(local [(define (response-generator make-url)
`(html
(body (h1 "Phase 1")
(a ((href ,(make-url phase-2)))
"click me!"))))]
(send/suspend/dispatch response-generator)))
@code:comment{phase-2: request -> html-response}
(define (phase-2 request)
(local [(define (response-generator make-url)
`(html
(body (h1 "Phase 2")
(a ((href ,(make-url phase-1)))
"click me!"))))]
(send/suspend/dispatch response-generator)))
]
This is a web application that goes round and round. When a user
first visits the application, the user starts off in @scheme[phase-1]. The
page that's generated has a hyperlink that, when clicked, continues to
@scheme[phase-2]. The user can click back, and falls back to @scheme[phase-1], and the
cycle repeats.
Let's look more closely at the @scheme[send/suspend/dispatch] mechanism.
@scheme[send/suspend/dispatch] consumes a response-generating function, and it
gives that response-generator a function called @scheme[make-url] that we'll
use to build special URLs. What makes these URLs special is this:
when a web browser visits these URLs, our web application restarts,
but not from start, but from the handler that we associate to the URL.
In @scheme[phase-1], the use of @scheme[make-url] associates the link with @scheme[phase-2], and
vice versa.
We can be more sophisticated about the handlers associated with
@scheme[make-url]. Because the handler is just a request-consuming function,
it can be defined within a @scheme[local]. Consequently, a local-defined
handler knows about all the variables that are in the scope of its
definition. Here's another loopy example:
@schememod[
web-server/insta
@code:comment{start: request -> html-response}
(define (start request)
(show-counter 0 request))
@code:comment{show-counter: number request -> html-response}
@code:comment{Displays a number that's hyperlinked: when the link is pressed,}
@code:comment{returns a new page with the incremented number.}
(define (show-counter n request)
(local [(define (response-generator make-url)
`(html (head (title "Counting example"))
(body
(a ((href ,(make-url next-number-handler)))
,(number->string n)))))
(define (next-number-handler request)
(show-counter (+ n 1) request))]
(send/suspend/dispatch response-generator)))
]
This example shows that we can accumulate the results of an
interaction. Even though the user start off by visiting and seeing
zero, the handlers produced by next-number-handler continue the
interaction, accumulating a larger and larger number.
Now that we've been going a little bit in circles, let's move forward
back to the blog application. We will adjust the form's action so it
directs to a URL that's associated to a separate handler.
@external-file["iteration-3.ss"]
Note that the structure of the @scheme[render-blog-page] function looks very
similar to that of our last @scheme[show-counter] example. The user can
finally add and see multiple posts to their blog.
Unfortunately, there's still a problem. To see the problem: add a few
posts to the system, and then open up a new browser window. In the
new browser window, visit the web application's URL. What happens?
@section{Share and Share Alike}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-4)]
We have run into another flaw with our application: each browser
window keeps track of its own distinct blog. That defeats the point a
blog for most people, that is, to share with others! When we insert a
new post, rather than create a new blog value, we'd like to make a
structural change to the existing blog. (HTDP Chapter 41). So
let's add mutation into the mix.
There's one small detail we need to touch: in the web-server language,
structures are immutable by default. We'll want to override this
default and get get access to the structure mutators. To do so, we
adjust our structure definitions with the @scheme[#:mutable] keyword.
Earlier, we had said that a @scheme[blog] was a list of @scheme[post]s,
but because we want to allow the blog to be changed, let's revisit our
definition so that a blog is a mutable structure:
@schemeblock[(define-struct blog (posts) #:mutable)]
@defstruct[blog ([posts (listof post?)])]
Mutable structures provide functions to change the fields of a
structure; in this case, we now have a structure mutator called
@scheme[set-blog-posts!],
@defthing[set-blog-posts! (blog? (listof post?) . -> . void)]
and this will allow us to change the posts of a blog.
@bold{Exercise.} Write a function @scheme[blog-insert-post!]
@defthing[blog-insert-post! (blog? post? . -> . void)]
The intended side effect of the function will be to extend the blog's
posts.
@centerline{------------}
Since we've changed the data representation of a blog, we'll need to
revise our web application to use the updated representation. One
other thing to note is that, within the web application, because we're
sharing the same blog value, we don't need to pass it around with our
handlers anymore: we can get at the current blog through our @scheme[BLOG]
variable.
After doing the adjustments incorporating @scheme[insert-blog-post!], and doing
a little variable cleanup, our web application now looks like this:
@external-file["iteration-4.ss"]
Open two windows that visit our web application, and start adding in
posts from both windows. We should see that both browsers are sharing
the same blog.
@section{Extending the Model}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-5)]
Next, let's extend the application so that each post can hold a list
of comments. We refine the data definition of a blog to be:
@defstruct[post ([title string?] [body string?] [comments (listof string?)]) #:mutable]
@bold{Exercise.} Write the updated data structure definition for posts. Make
sure to make the structure mutable, since we intend to add comments to
posts.
@bold{Exercise.} Make up a few examples of posts.
@bold{Exercise.} Define a function @scheme[post-add-comment!]
@defthing[post-add-comment! (post? string? . -> . void)]
whose intended side effect is to add a new comment to the end of the post's
list of comments.
@bold{Exercise.} Adjust @scheme[render-post] so that the produced fragment will include the
comments in an itemized list.
@bold{Exercise.} Because we've extended a post to include comments, other
post-manipulating parts of the application may need to be adjusted,
such as uses of @scheme[make-post]. Identify and fix any other part of the
application that needs to accommodate the post's new structure.
@centerline{------------}
Once we've changed the data structure of the posts and adjusted our
functions to deal with this revised structure, the web application
should be runnable. The user may even may even see some of the fruits
of our labor: if the initial @scheme[BLOG] has a post with a comment, the user
should see those comments now. But obviously, there's something
missing: the user doesn't have the user interface to add comments to a
post!
@section{Breaking Up the Display}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-5)]
How should we incorporate comments more fully into the user's web
experience? Seeing all the posts and comments all on one page may
be a bit overwhelming. Perhaps we should hold off on showing the
comments on the main blog page. Let's present a secondary "detail"
view of a post, and present the comments there.
The top-level view of a blog then can show the blog's title and body.
We can also show a count of how many comments are associated to the
post.
So now we need some way to visit a post's detail page. One way to do
this is to hyperlink each post's title: if the user wants to see the
detail page of a post, user should be able to click the title to get
there. From that post's detail page, we can even add a form to let
the user add new comments.
Here's a diagram of a simple page flow of our web application that
should let us add comments.
@image{scribblings/tutorial/images/flow1.png}
Each point in the diagram corresponds to a request-consuming handler.
As we might suspect, we'll be using @scheme[send/suspend/dispatch] some more.
Every arrow in the diagram will be realized as a URL that we generate
with @scheme[make-url].
This has a slightly messy consequence: previously, we've been
rendering the list of posts without any hyperlinks. But since any
function that generates a special dispatching URL uses make-url to do
it, we'll need to adjust @scheme[render-posts] and @scheme[render-post] to consume and
use @scheme[make-url] itself when it makes those hyperlinked titles.
Our web application now looks like:
@external-file["iteration-5.ss"]
We now have an application that's pretty sophisticated: we can add
posts and write comments. Still, there's a problem with this: once
the user's in a @scheme[post-detail-page], they can't get back to the blog
without pressing the browser's back button! That's disruptive. We
should provide a page flow that lets us get back to the main
blog-viewing page, to keep the user from every getting "stuck" in a
dark corner of the web application.
@section{Adding a Back Button}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-6)]
Here's a diagram of a our revised page flow of our web application.
Maybe we can just add a BACK link from the @scheme[render-post-detail-page]
that gets us back to viewing the top-level blog.
@image{scribblings/tutorial/images/flow2.png}
@bold{Exercise.} Adjust @scheme[render-post-detail-page] to include another link that goes
back to @scheme[render-blog-page].
To make this more interesting, maybe we should enrich the flow a
bit more. We can give the user a choice right before committing to
their comment. Who knows? They may have a change of heart.
@image{scribblings/tutorial/images/flow3.png}
Although this seems complicated, the shape of our handlers will look
more-or-less like what we had before. After we've added all the
handlers, our web application is fairly functional.
@external-file["iteration-6.ss"]
@section{Decorating With Style!}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-7
web-server/insta/insta)]
We have an application that's functionally complete, but is visual
lacking. Let's try to improve its appearance. One way we can do this
is to use a cascading style sheet. A style sheet can visual panache
to our web pages. For example, if we'd like to turn all of our
paragraphs green, we might add the following style declaration within
our response.
@scheme['(style ((type "text/css")) "p { color: green }")]
It's tempting to directly embed this style information into our
@scheme[html-response]s. However, our source file is already quite busy. We
often want to separate the logical representation of our application
from its presentation. Rather than directly embed the .css in the
HTML response, let's instead add a link reference to an separate .css
file.
Until now, all the content that our web application has produced has
come from a response generating handler. Of course, we know that not
everything needs to be dynamically generated: it's common to have
files that won't be changing. We should be able to serve these static
resources (images, documents, .css files) alongside our web
applications.
To do this, we set aside a path to store these files, and then tell
the web server where that directory is. The function
@scheme[static-files-path],
@defthing[static-files-path (path? -> void)]
tells the web server to look in the given path when it receives a URL
that looks like a static resource request.
@bold{Exercise.} Create a simple web application called @filepath{test-static.ss} with the
following content:
@schememod[
web-server/insta
(define (start request)
'(html (head (title "Testing"))
(link ((rel "stylesheet")
(href "/test-static.css")
(type "text/css")))
(body (h1 "Testing")
(h2 "This is a header")
(p "This is " (span ((class "hot")) "hot") "."))))
(static-files-path "htdocs")
]
Make a subdirectory called @filepath{htdocs} rooted in the same directory as
the @filepath{test-static.ss} source. Finally, just to see that we can serve
this .css page, create a very simple .css file @filepath{test-static.css} file
in @filepath{htdocs/} with the following content:
@verbatim{
body {
margin-left: 10%;
margin-right: 10%;
}
p { font-family: sans-serif }
h1 { color: green }
h2 { font-size: small }
span.hot { color: red }
}
At this point, run the application and look at the browser's output.
We should see a Spartan web page, but it should still have some color
in its cheeks.
@centerline{------------}
@bold{Exercise.}
Improve the presentation of the blog web application by writing your
an external style sheet to your tastes. Adjust all of the HTML
response handlers to include a link to the style sheet.
@section{The Double Submit Bug}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-7
web-server/servlet)]
There's yet another a subtle problem in our application. To see it,
bring our blog application up again, and add a post. Then reload the
page. Reload the page again.
What's happening is a well-known problem: it's an instance of the
"double-submit" problem. If a user presses reload, a request is sent
over to our application. This wouldn't be such a bad thing, if not
for the fact that we're handling certain requests by mutating our
application's data structures.
A common pattern that web developers use to dodge the double
submission problem is to handle state-mutating request in a peculiar
way: once the user sends over a request that affects change to the
system, we then redirect them off to a different URL that's safe to
reload. To make this happen, we will use the function @scheme[redirect/get].
@defthing[redirect/get (-> request?)]
This @scheme[redirect/get] function has an immediate side effect: it forces the
user's browser to follow a redirection to a safe URL, and gives us
back that fresh new request.
For example, let's look at a toy application that lets the users add
names to a roster:
@external-file["no-use-redirect.ss"]
This application suffers the same problem as our blog: if the user
adds a name, and then presses reload, then the same name will be added
twice.
We can fix this by changing a single expression. Can you see what
changed?
@external-file["use-redirect.ss"]
Double-submit, then, is painlessly easy to mitigate. Whenever we have
handlers that mutate the state of our system, we use @scheme[redirect/get] when
we send our response back.
@bold{Exercise.}
Revise the blog application with @scheme[redirect/get] to address the
double-submit problem.
With these minor fixes, our blog application now looks like this:
@external-file["iteration-7.ss"]
@section{Abstracting the Model}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-8
web-server/scribblings/tutorial/examples/model)]
If we "turn off the lights" by closing the program, then the state of
our application disappears into the ether. How do we get our
ephemeral state to stick around? Before we tackle that question, we
should consider: what do we want to save? There's some state that we
probably don't have a lingering interest in, like requests. What we
care about saving is our model of the blog.
If we look closely at our web application program, we see a seam
between the model of our blog, and the web application that uses that
model. Let's isolate the model: it's all the stuff near the top:
@schemeblock[
(define-struct blog (posts) #:mutable)
(define-struct post (title body comments) #:mutable)
(define BLOG ...)
(define (blog-insert-post! ...) ...)
(define (post-insert-comment! ...) ...)
]
In realistic web applications, the model and the web application are
separated by some wall of abstraction. The theory is that, if we do
this separation, it should be easier to then make isolated changes
without breaking the entire system. Let's do this: we will first rip
the model out into a separate file. Once we've done that, then we'll
look into making the model persist.
Create a new file called @filepath{model.ss} with the following content.
@external-file["model.ss"]
This is essentially a cut-and-paste of the lines we identified as our
model. It's written in the @schememodname[scheme] language because
the model shouldn't need to worry about web-server stuff. There's one
additional expression that looks a little odd at first:
@schemeblock[
(provide (all-defined-out))
]
which tells PLT Scheme to allow other files to have access to
everything that's defined in the @filepath{model.ss} file.
We change our web application to use this model. Going back to our
web application, we rip out the old model code, and replace it with an
expression that let's use use the new model.
@schemeblock[
(require "model.ss")
]
which hooks up our web application module to the @schememodname["model.ss"] module.
@external-file["iteration-8.ss"]
@section{A Model of Persistent}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/iteration-9
web-server/scribblings/tutorial/examples/model-2)]
Now that the model is separated into a separate module, we can more easily modify
its functionality, and in particular, make it persistent.
The first step is to make the model structures serializable. Earlier, we made the
structures mutable by adding @scheme[#:mutable] to their definitions. We can make
the structures serializable by adding @scheme[#:prefab]. This tells PLT Scheme that
these structures can be "previously fabricated", that is, created before the program
started running---which is exactly what we want when restoring the blog data from disk.
Our blog structure definition now looks like:
@schemeblock[
(define-struct blog (posts) #:mutable #:prefab)
]
Now @scheme[blog] structures can be read from the outside world with @scheme[read] and written
with @scheme[write]. However, we need to make sure everything inside a @scheme[blog] structure is
also marked as @scheme[#:prefab]. If we had a more complicated structure, we would need to ensure
that everything (transitively) in the structure was @scheme[#:prefab]'d.
@bold{Exercise.} Write the new structure definition for posts.
At this point, we @emph{can} read and write the blog to disk. Now let's actually do it.
First, we'll make a place to record in the model where the blog lives on disk. So, we need to change
the blog structure again. Now it will be:
@defstruct[blog ([home string?] [posts (listof post?)]) #:mutable]
@bold{Exercise.} Write the new structure definition for blogs.
Then, we'll make a function that allows our application to initialize the blog:
@schemeblock[
@code:comment{initialize-blog! : path? -> blog}
@code:comment{Reads a blog from a path, if not present, returns default}
(define (initialize-blog! home)
(local [(define (log-missing-exn-handler exn)
(make-blog
(path->string home)
(list (make-post "First Post"
"This is my first post"
(list "First comment!"))
(make-post "Second Post"
"This is another post"
(list)))))
(define the-blog
(with-handlers ([exn? log-missing-exn-handler])
(with-input-from-file home read)))]
(set-blog-home! the-blog (path->string home))
the-blog))
]
@scheme[initialize-blog!] takes a path and tries to @scheme[read] from it. If the path contains
a @scheme[blog] structure, then @scheme[read] will parse it, because @scheme[blog]s are @scheme[#:prefab].
If there is no file at the path, or if the file has some spurious data, then @scheme[read] or
@scheme[with-input-from-file] will throw an exception. @scheme[with-handlers] provides an
exception handler that will return the default @scheme[blog] structure for all
kinds of errors.
After @scheme[the-blog] is bound to the newly read (or default) structure, we set the home to the
correct path. (Notice that we need to convert the path into a string. Why didn't we just make the blog
structure contain paths? Answer: They can't be used with @scheme[read] and @scheme[write].)
Next, we will need to write a function to save the model to the disk.
@schemeblock[
@code:comment{save-blog! : blog -> void}
@code:comment{Saves the contents of a blog to its home}
(define (save-blog! a-blog)
(local [(define (write-to-blog)
(write a-blog))]
(with-output-to-file (blog-home a-blog)
write-to-blog
#:exists 'replace)))
]
@scheme[save-blog!] @scheme[write]s the model into its home .
It provides @scheme[with-output-to-file] with an @scheme[#:exists] flag that tells it to replace the
file contents if the file at @scheme[blog-home] exists.
This function can now be used to save the blog structure whenever we modify it. Since we only ever modify the
blog structure in the model, we only need to update @scheme[blog-insert-post!] and @scheme[post-insert-comment!].
@bold{Exercise.} Change @scheme[blog-insert-post!] and @scheme[post-insert-comment!] to call @scheme[save-blog!].
@centerline{------------}
You may have had a problem when trying to update @scheme[post-insert-comment!]. It needs to call @scheme[save-blog!]
with the blog structure. But, it wasn't passed the blog as an argument. We'll need to add that argument and change the
application appropriately. While we're at it, let's change @scheme[blog-insert-post!] to accept the contents of the
post structure, rather the structure itself, to better abstract the model interface:
@defthing[blog-insert-post! (blog? string? string? . -> . void)]
@defthing[post-insert-comment! (blog? post? string? . -> . void)]
@bold{Exercise.} Write the new definitions of @scheme[blog-insert-post!] and @scheme[post-insert-comment!].
(Remember to call @scheme[save-blog!].)
In our last iteration of our model, we used @scheme[(provide
(all-defined-out))] to expose all of the model's definitions. But we
often want to hide things like private functions and internal data
structures from others. We'll do that here by using a form of
@scheme[provide] that explicitly names the exposed definitions.
For example, if we wanted to limit the exposed functions to
@scheme[blog-insert-post!] and @scheme[post-insert-comment!], we can
do this:
@schemeblock[
(provide blog-insert-post!
post-insert-comment!)
]
Of course, this set of functions is too minimal! Let's change the
@scheme[provide] line in the model to:
@schemeblock[
(provide blog? blog-posts
post? post-title post-body post-comments
initialize-blog!
blog-insert-post! post-insert-comment!)
]
which captures the essential interactions we do with a blog.
@centerline{------------}
The last step is to change the application. We need to call @scheme[initialize-blog!] to read in the blog structure, and we
need to pass the blog value that is returned around the application, because there is no longer a @scheme[BLOG] export.
First, change @scheme[start] to call @scheme[initialize-blog!] with a path in our home directory:
@schemeblock[
(define (start request)
(render-blog-page
(initialize-blog!
(build-path (find-system-path 'home-dir)
"the-blog-data.db"))
request))
]
@bold{Exercise.} Thread the @scheme[blog] structure through the application appropriately to give
@scheme[blog-insert-post!] and @scheme[post-insert-comment!] the correct values. (You'll also need to
change how @scheme[render-blog-page] adds new posts.)
@centerline{------------}
Our model is now:
@external-file["model-2.ss"]
And our application is:
@external-file["iteration-9.ss"]
@centerline{------------}
This approach to persistence can work surprisingly well for simple applications. But as our application's needs
grow, we will have to deal with concurrency issues, the lack of a simply query language over our data model, etc.
So, in the next section, we'll talk about how to use an SQL database to store our blog model.
@section{Using an SQL database}
@declare-exporting[#:use-sources (web-server/scribblings/tutorial/examples/dummy-10
web-server/scribblings/tutorial/examples/dummy-3
web-server/scribblings/tutorial/dummy-sqlite)]
@(require (for-label web-server/scribblings/tutorial/dummy-sqlite))
@;@(require (prefix-in sqlite: (for-label (planet jaymccarthy/sqlite:3/sqlite))))
Our next task is to employ an SQL database for the blog model. We'll be using SQLite with the @schememodname[(planet jaymccarthy/sqlite:3/sqlite)] PLaneT package. We add the following to the top of our model:
@schemeblock[
(require (prefix-in sqlite: (planet jaymccarthy/sqlite:3/sqlite)))
]
We now have the following bindings:
@defthing[sqlite:db? (any/c . -> . boolean?)]
@defthing[sqlite:open (path? . -> . sqlite:db?)]
@defthing[sqlite:exec/ignore (sqlite:db? string? . -> . void)]
@defthing[sqlite:select (sqlite:db? string? . -> . (listof vector?))]
@defthing[sqlite:insert (sqlite:db? string? . -> . integer?)]
The first thing we should do is decide on the relational structure of our model. We will use the following tables:
@verbatim{
CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, body TEXT)
CREATE TABLE comments (pid INTEGER, content TEXT)
}
Each post will have an identifier, a title, and a body. This is the same as our old Scheme structure,
except we've added the identifier. (Actually, there was always an identifier---the memory pointer---but now
we have to make it explicit in the database.)
Each comment is tied to a post by the post's identifier and has textual content. We could have chosen to
serialize comments with @scheme[write] and add a new TEXT column to the posts table to store the value.
By adding a new comments table, we are more in accord with the relational style.
A @scheme[blog] structure will simply be a container for the database handle:
@defstruct[blog ([db sqlite:db?])]
@bold{Exercise.} Write the @scheme[blog] structure definition. (It does not need to be mutable or serializable.)
We can now write the code to initialize a @scheme[blog] structure:
@schemeblock[
@code:comment{initialize-blog! : path? -> blog?}
@code:comment{Sets up a blog database (if it doesn't exist)}
(define (initialize-blog! home)
(define db (sqlite:open home))
(define the-blog (make-blog db))
(with-handlers ([exn? void])
(sqlite:exec/ignore db
(string-append
"CREATE TABLE posts "
"(id INTEGER PRIMARY KEY,"
"title TEXT, body TEXT)"))
(blog-insert-post!
the-blog "First Post" "This is my first post")
(blog-insert-post!
the-blog "Second Post" "This is another post")
(sqlite:exec/ignore
db "CREATE TABLE comments (pid INTEGER, content TEXT)")
(post-insert-comment!
the-blog (first (blog-posts the-blog))
"First comment!"))
the-blog)
]
@scheme[sqlite:open] will create a database if one does not already exist at the @scheme[home] path. But, we still need
to initialize the database with the table definitions and initial data.
We used @scheme[blog-insert-post!] and @scheme[post-insert-comment!] to initialize the database. Let's see their implementation:
@schemeblock[
@code:comment{blog-insert-post!: blog? string? string? -> void}
@code:comment{Consumes a blog and a post, adds the post at the top of the blog.}
(define (blog-insert-post! a-blog title body)
(sqlite:insert
(blog-db a-blog)
(format "INSERT INTO posts (title, body) VALUES ('~a', '~a')"
title body)))
@code:comment{post-insert-comment!: blog? post string -> void}
@code:comment{Consumes a blog, a post and a comment string. As a side-effect,}
@code:comment{adds the comment to the bottom of the post's list of comments.}
(define (post-insert-comment! a-blog p a-comment)
(sqlite:insert
(blog-db a-blog)
(format
"INSERT INTO comments (pid, content) VALUES ('~a', '~a')"
(post-id p) a-comment)))
]
@bold{Exercise.} Find the security hole common to these two functions.
@centerline{------------}
A user could submit a post with a title like, @scheme{null', 'null') and INSERT INTO accounts (username, password) VALUES ('ur','hacked} and get our simple @scheme[sqlite:insert] to make two INSERTs instead of one.
This is called an SQL injection attack. It can be resolved by using
prepared statements that let SQLite do the proper quoting for us. Refer
to the SQLite package documentation for usage.
@centerline{------------}
In @scheme[post-insert-comment!], we used @scheme[post-id], but we have not yet defined the new @scheme[post] structure.
It @emph{seems} like a @scheme[post] should be represented by an integer id, because the post table contains an integer as the identifying value.
However, we cannot tell from this structure
what blog this posts belongs to, and therefore, what database; so, we could not extract the title or body values,
since we do not know what to query. Therefore, we should associate the blog with each post:
@defstruct[post ([blog blog?] [id integer?])]
@bold{Exercise.} Write the structure definition for posts.
The only function that creates posts is @scheme[blog-posts]:
@schemeblock[
@code:comment{blog-posts : blog -> (listof post?)}
@code:comment{Queries for the post ids}
(define (blog-posts a-blog)
(local [(define (row->post a-row)
(make-post
a-blog
(string->number
(vector-ref a-row 0))))
(define rows (sqlite:select
(blog-db a-blog)
"SELECT id FROM posts"))]
(cond [(empty? rows)
empty]
[else
(map row->post (rest rows))])))
]
@scheme[sqlite:select] returns a list of vectors. The first element of the list is the name of the columns.
Each vector has one element for each column. Each element is a string representation of the value.
At this point we can write the functions that operate on posts:
@schemeblock[
@code:comment{post-title : post -> string?}
@code:comment{Queries for the title}
(define (post-title a-post)
(vector-ref
(second
(sqlite:select
(blog-db (post-blog a-post))
(format
"SELECT title FROM posts WHERE id = '~a'"
(post-id a-post))))
0))
]
@bold{Exercise.} Write the definition of @scheme[post-body].
@bold{Exercise.} Write the definition of @scheme[post-comments].
(Hint: Use @scheme[blog-posts] as a template, not @scheme[post-title].)
@centerline{------------}
The only change that we need to make to the application is to require the new model. The interface is exactly the same!
@centerline{------------}
Our model is now:
@external-file["model-3.ss"]
And our application is:
@schememod[
web-server/insta
(require "model-3.ss")
....
]
@section{Leaving DrScheme}
So far, to run our application, we've been pressing @onscreen{Run} in DrScheme. If we were to actually deploy
an application, we'd need to do this differently.
@(require (for-label web-server/servlet-env)
(for-label web-server/managers/lru))
The simplest way to do this is to use @schememodname[web-server/servlet-env].
First, change the first lines in your application from
@schememod[
web-server/insta
]
to
@schememod[
scheme
(require web-server/servlet)
(provide/contract (start (request? . -> . response?)))
]
Second, add the following at the bottom of your application:
@schemeblock[
(require web-server/servlet-env)
(serve/servlet start
#:launch-browser? #f
#:quit? #f
#:listen-ip #f
#:port 8000
#:extra-files-path
(build-path _path "htdocs")
#:servlet-path
"servlets/APPLICATION.ss")
]
You can change the value of the @scheme[#:port] parameter to use a different port.
@scheme[#:listen-ip] is set to @scheme[#f] so that the server will listen on @emph{all} available IPs.
You should change @scheme[_path] to be the path to the parent of your @scheme[htdocs] directory.
You should change @scheme["APPLICATION.ss"] to be the name of your application.
Third, to run your server, you can either press @onscreen{Run} in DrScheme, or type
@commandline{mzscheme -t <file.ss>}
(With your own file name, of course.) Both of these will start a Web server @emph{just} for your application.
@centerline{------------}
@scheme[serve/servlet] takes other options and there are more advanced ways of starting the Web Server,
but you'll have to refer to the PLT Web Server Reference Manual for details.
@section{Using HTTPS}
This final task that we'll cover is using the server in HTTPS mode.
This requires an SSL certificate and private key. This is very platform specific, but we will provide
the details for using OpenSSL on UNIX:
@commandline{openssl genrsa -des3 -out host.key 1024}
This will generate a new private key, but it will have a passphrase on it. You can remove this via:
@commandline{openssl rsa -in host.key -out host.key}
@commandline{chmod 400 host.key}
Now, we generate a self-signed certificate:
@commandline{openssl req -new -x509 -nodes -sha1 -days 365 -key host.key > host.cert}
(Each certificate authority has different instructions for generating certificate signing requests.)
If we move these files into our home directory:
@commandline{mv host.key host.cert ~/}
We can now start the server with the following incantation:
@(require (for-label scheme/unit)
(for-label net/ssl-tcp-unit)
(for-label net/tcp-sig)
(for-label net/tcp-unit)
(for-label web-server/web-server)
(for-label web-server/web-server-unit)
(for-label web-server/web-server-sig)
(for-label web-server/web-config-sig)
(for-label web-server/web-config-unit)
(for-label web-server/configuration/namespace))
@schememod[
scheme
@code:comment{Load the appropriate libraries to reimplement server}
(require scheme/unit
net/ssl-tcp-unit
net/tcp-sig
net/tcp-unit
web-server/web-server
web-server/web-server-unit
web-server/web-server-sig
web-server/web-config-sig
web-server/web-config-unit
web-server/configuration/namespace)
@code:comment{Define the necessary parameters.}
(define port-no 8443)
(define SSL-path (find-system-path 'home-dir))
@code:comment{Load the standard configuration file, but augment the port.}
(define configuration
(configuration-table-sexpr->web-config@
(build-path (collection-path "web-server")
"default-web-root"
"configuration-table.ss")
#:port port-no))
@code:comment{The configuration is a unit and this lets us treat it as one.}
(define-unit-binding config@ configuration
(import) (export web-config^))
@code:comment{This loads the SSL TCP interface with the appropriate keys.}
(define-unit-binding ssl-tcp@
(make-ssl-tcp@ (build-path SSL-path "host.cert")
(build-path SSL-path "host.key")
#f #f #f #f #f)
(import) (export tcp^))
@code:comment{Combine the configuration with the TCP interface to get a server!}
(define-compound-unit/infer ssl-server@
(import)
(link ssl-tcp@ config@ web-server@)
(export web-server^))
@code:comment{Invoke the server to get at what it provides.}
(define-values/invoke-unit/infer ssl-server@)
@code:comment{Run the server.}
(serve)
(do-not-return)
]
This is, admittedly, not the simplest imaginable way of setting up a server, but it gets the job done.
@section{Moving Forward}
As you move forward on your own applications, you may find many useful packages on PLaneT. There are interfaces to other
databases. Many tools for generating HTML, XML, and Javascript output. Etc. There is also an active community of
users on the @scheme[plt-scheme] mailing list. We welcome new users!