woomera 3.0.1

  • README.md
  • CHANGELOG.md
  • Example
  • Installing
  • Versions
  • 50

Woomera

Introduction

Woomera is a Dart package for implementing Web servers.

It is used to create server-side Dart programs that function as a Web server. A Web server listens for HTTP requests and respond to them with HTTP responses: a simple task, but one that can get complicated (and difficult to maintain) when the program has many different pages to display, handle errors and maintain state. This package aims to reduce that complexity.

Main features include:

  • URL pattern matching inspired by the Sinatra Web framework - allows easy parsing of URL path components as parameters;

  • Exception handling framework - ensures error pages are reliably generated and unexpected exceptions are always "caught" to generate an error page response;

  • Pipelines of patterns for matching against URLs to allow sophisticated processing, if needed - allows requests to be processed by multiple handlers (e.g. to log/audit requests before handling them) and different exception handlers to be set for different resources;

  • Session management using cookies or URL rewriting;

  • Responses can be generated into a buffer - allows response to contain a complete error page instead of an incompletely generated result page.

  • Responses can be read from a stream of data.

Note: This version requires Dart 2. Please use version "<3.0.0" if running Dart 1.

This following is a tutorial which provides an overview the main features of the package. For details about the package and its advanced features, please see the API documentation.

Tutorial

1. A basic Web server

1.1. Overview

This is a basic Web server that serves up one page. It creates a server with one response handler.

import 'dart:async';
import 'dart:io';

import 'package:woomera/woomera.dart';

Future main() async {
  // Create and configure server

  var ws = new Server();
  ws.bindAddress = InternetAddress.ANY_IP_V6;
  ws.bindPort = 1024;

  // Register rules

  var p = ws.pipelines.first;
  p.get("~/", handleTopLevel);

  // Run the server

  await ws.run();
}

Future<Response> handleTopLevel(Request req) async {
  var name = req.queryParams["name"];
  name = (name.isEmpty) ? "world" : name;

  var resp = new ResponseBuffered(ContentType.HTML);
  resp.write("""
<html>
  <head>
    <title>Woomera Tutorial</title>
  </head>
  <body>
    <h1>Hello ${HEsc.text(name)}!</h1>
  </body>
</html>
""");
  return resp;
}

The most important feature of the package is to organise response handlers, so that HTTP requests can be matched to Dart code to process them and to generate a HTTP response.

A Server has of a sequence of pipelines, and each pipeline has a sequence of rules. Each rule consists of the HTTP method (e.g. GET or POST), a path pattern, and a request handler method.

When a HTTP request arrives, the pipelines are search (in order) for a rule that matches the request. A match is when the HTTP method is the same and the pattern matches the request URL's path. If found, the corresponding handler is invoked to produce the HTTP response. If no rule is found (after searching through all the rules in all the pipelines), the resource is treated as not found.

1.2. Importing the package

Any program that uses the framework must first import the package:

import 'package:woomera/woomera.dart';

1.3. The server

For the Web server, a Server object is created and configured for the TCP/IP address and port it will listen for HTTP requests on.

var ws = new Server();
ws.bindAddress = InternetAddress.ANY_IP_V6;
ws.bindPort = 1024;

For testing, the above example sets it to InternetAddress.ANY_IP_V6, so the service is listening to connections on any interface (i.e. both loopback and public). When using InternetAddress.ANY_IP_V6, the v6Only member is used to control whether IPv4 addresses are included or not.

Typically, when deployed in production, the service is accessed via a reverse Web proxy (e.g. Apache or Nginx). The default bind address is InternetAddress.LOOPBACK_IP_V4, which means it only listens for connections on 127.0.0.1. That is, only clients on the same host can connect to it.

A port number 1024 or greater should be used, because the lower port numbers are require special permission to use.

1.4. The pipeline

The Server (by default) automatically creates one pipeline, since that is the most common scenario. The pipelines member is a List of ServerPipeline objects, so retrieve it from the server using something like:

var p = ws.pipelines.first;

1.5. The rules

Rules are registered with the pipeline. The get method on the ServerPipeline object will register a rule for the HTTP GET method, and the post method will register a rule for the HTTP POST method. The first parameter is the pattern. The second parameter is the handler method: the method that gets invoked when the rule matches the HTTP request.

p.get("~/", handlerTopLevel);

The tilde ("") indicates this is relative to the base path of the server. The default base path is "/". See the API documentation for information about changing the base path. For now, all paths should begin with "/".

1.6. Running the server

After configuring the [Server], start it using its run method. The run method returns a Future that completes when the Web server finishes running; but normally a Web server runs forever without stopping.

await ws.run();

1.7. Request handlers

A request handler method is used to process the HTTP request to produce a HTTP response. It is passed the HTTP request as a Request object; and it returns a HTTP response as represented by a Response object.

There are different types of Response objects. The commonly used one for generating HTML pages is the ResponseBuffered. It acts as a buffer where the contents is appended to it using the write method. After the response is returned from the request handler, the framework uses it to generate the HTTP response that is sent back to the client.

This first example request handler returns a simple HTML page.

Future<Response> handleTopLevel(Request req) async {
  var name = req.queryParams["name"];
  name = (name.isEmpty) ? "world" : name;

  var resp = new ResponseBuffered(ContentType.HTML);
  resp.write("""
<html>
  <head><title>Example 1</title></head>
  <body>
    <h1>Hello ${HEsc.text(name)}!</h1>
  </body>
</html>
""");
  return resp;
}

The "name" query parameter is retrieved from the request. If it is the empty string, a default constant value is used instead. The square bracket operator returns the empty string if the parameter does not exist.

The name is used in the HTML heading. The HEsc.text method is used to escape any special characters, to prevent accidential or malicious HTML injection.

When a Web browser sends a request to the site's URL the HTML page is returned. In this document, the example URLs will show the hostname of the server as "localhost"; if necessary, change it to the hostname or IP address of the machine running your server.

Run the server and try visiting:

The last example demonstrates the importance of using HEsc.text to escape values.

Also visit something like http://localhost:1024/nosuchpage and the basic built-in error page appears. To customize the error page, a custom exception handler is used.

1.8. Exception handler

An exception handler processes any exceptions that are raised: either by one of the request handlers or by the framework.

It is similar to a request handler, because it is a method that returns a Response object. But it is different, because it is also passed the exception and sometimes a stack trace.

When setting up the server, set its exception handler in main (anywhere before the server is run):

ws.exceptionHandler = myExceptionHandler;

And define the exception handler method as:

Future<Response> myExceptionHandler(Request req, Object ex, StackTrace st) async {
  var status;
  var message;

  if (ex is NotFoundException) {
    status = (ex.found == NotFoundException.foundNothing) ? HttpStatus.METHOD_NOT_ALLOWED : HttpStatus.NOT_FOUND;
    message = "Sorry, the page you were looking for could not be found.";
  } else {
    status = HttpStatus.INTERNAL_SERVER_ERROR;
    message = "Sorry, an internal error occured.";
    print("Exception: $ex");
  }

  var resp = new ResponseBuffered(ContentType.HTML);
  resp.status = status;

  resp.write("""
<html>
  <head>
    <title>Error</title>
  </head>
  <body>
    <h1>Error</h1>
    <p>$message</p>
  </body>
</html>
""");

  return resp;
}

This exception handler customizes the error page when the NotFoundException is encountered: it is raised when none of the rules matched the request. Notice that it reports a different status code if no rules for the method could be found (405 method not allowed), versus when some rules for the method exist but their pattern did not match the requested path (404 not found).

Other exceptions can be detected and handled differently. But in this example, they all produce the same error page.

Run this server and visit http://localhost:1024/nosuchpage to see the custom error page.

2. HTML escaping methods

The HEsc class defines three static methods which are useful for converting objects into Strings that are then escaped for embedded into HTML.

  • attr for escaping values to be inserted into attributes.
  • text for escaping values to be inserted into element content.
  • lines which is the same as text, but adds line breaks elements (i.e. <br/>) where newlines exist in the original value.

These methods will be used to escape values which might contain characters with special meaning in HTML.

3. Parameters

The request handler methods can receive three different types of parameters:

  • path parametrs;
  • query parameters; and
  • post parameters.

3.1. Path parameters

The path parameters are extracted from the path of the URL being requested.

The path parameters are defined by the rule's pattern, which is made up of components separated by a slash ("/"). Path parameters are represented by a component starting with a colon (":") followed by the name of the parameter.

The path parameters are made available to the handler via the pathParams member of the Request object.

This is an example of a rule with a fixed path, where each component must match the requested URL exactly and there are no path parameters.

p.get("~/foo/bar/baz", handleParams);

This is an example with a single parameter:

p.get("~/user/:name", handleParams);

This is an example with two parameters:

p.get("~/user/:name/:orderNumber", handleParams);

The wildcard is a special path parameter that will match zero or more segments in the URL path.

p.get("~/product/*", handleParams);

Here is an example request handler that shows the parameters in the request.

Future<Response> handleParams(Request req) async {
  var resp = new ResponseBuffered(ContentType.HTML);
  resp.write("""
<html>
  <head>
    <title>Woomera Tutorial</title>
  </head>
  <body>
    <h1>Parameters</h1>
""");

  resp.write("<h2>Path parameters</h2>");
  _dumpParam(req.pathParams, resp);

  resp.write("<h2>Query parameters</h2>");
  _dumpParam(req.queryParams, resp);

  resp.write("<h2>POST parameters</h2>");
  _dumpParam(req.postParams, resp);

  resp.write("""
  </body>
</html>
""");
  return resp;
}

void _dumpParam(RequestParams p, ResponseBuffered resp) {
  if (p != null) {
    var keys = p.keys;

    if (keys.isNotEmpty) {
      resp.write("<p>Number of keys: ${keys.length}</p>");
      resp.write("<dl>");

      for (var k in keys) {
        resp.write("<dt>${HEsc.text(k)}</dt><dd><ul>");
        for (var v in p.values(k)) {
          resp.write("<li>${HEsc.text(v)}</li>");
        }
        resp.write("</ul></dd>");
      }

      resp.write("</dl>");
    } else {
      resp.write("<p>No parameters.</p>");
    }
  } else {
    resp.write("<p>Not available.</p>");
  }
}

Here are a few URLs to try:

3.2. Query parameters

The query parameters are the query parameters from the URL. That is, the name-value pairs after the question mark ("?").

The path parameters are made available to the handler via the queryParams member of the Request object. They are not (and cannot) be specified in the rule.

Here are a few URLs to try:

3.3. Post parameters

The post parameters are extracted from the contents of a HTTP POST request. Obviously, they are only available when processing a POST request.

The path parameters are made available to the handler via the postParams member of the Request object, which is null unless it is a POST request. They are not (and cannot) be specified in the rule.

For example, try this form:

<form method="POST" action="http://example.com/transaction">
  <input type="radio" name="type" value="out" id="w"/> <label for="w">Withdraw</label>
  <input type="radio" name="type" value="in" id="d"/> <label for="d">Deposit</label>
  <input type="text" name="amount"/>
</form>

processed by the above handler prints out:

"Hello World"

3.4. Common aspects

The three parameter members are instances of the RequestParams class.

It is important to remember that parameters can be repeated. For example, checkboxes on a form will result in one instance of the named parameter for every checkbox that is checked. This can apply to path parameters, query parameters and post parameters.

3.4.1. Retrieving parameters

The RequestParams class can be thought of as a Map, where the keys are the names of the parameters which maps into a List of values. If there is only one value, there is still a list: a list containing only one value.

The names of all the available parameters can be obtained using the keys method.

for (var k in req.queryParams.keys) {
  print("Got a query parameter named: $k");
}

All the values for a given key can be obtained using the values method.

for (var k in req.queryParams.keys) {
  var vList = req.queryParams.values(k);
  for (var v in vList) {
    print("$k = $v");
  }
}

If your request handler is expecting only one value, the square-bracket operator can be used to retrieve a single value instead of a list.

 var t = req.queryParams["title"];
3.4.2. Raw vs processed values

The methods described above for retrieving value(s) returns a cleaned up processed version of the value. The processing:

  • removes all leading whitespaces;
  • removes all trailing whitespace;
  • collapses multiple whitespaces in a row into a single whitespace; and
  • convert all whitespace characters into the space character.

To obtain the unprocessed value, set raw to true with the values method:

req.queryParams.values("category", raw: true);
3.4.3. Expecting the unexpected

To make a robust application, do not make any assumptions about what parameters may or may not be present: check everything and fail gracefully. The parameters might be different from what is expected because of programming errors, misuse or (worst case, but very important to deal with) the application is under malicious attack.

If a parameter is missing, the square bracket operator returns an empty string, and the values method returns an empty list when it is returning proceesed values. In raw mode, the values method returns null if the value does not exist: which is the only way to detect the difference between the presence of a blank/empty parameter versus the absence of the parameter.

An application might be designed to expect exactly one instance of a parameter, but a malicious client might try to send two or more values to break. The square bracket operator, which is used when only one value is expected, will return the empty string if the multiple copies of the parameter exist.

Both the names and values are always strings.

4. Exceptions

Exception handlers are a type of handler used to process exceptions that are raised. They are passed the request and the exception, and are expected to generate a Response. The exception handler should create a response that serves as an error page for the client.

Future<Response> myExceptionHandler(Request req
    Object exception, StackTrace st) async {
  var resp = new ResponseBuffered(ContentType.HTML);
  resp.write("""
<html>
  <head><title>Error</title></head>
  <body>
    <h1>Error</h1>
    <p>Sorry, an error occured: ${HEsc.text(exception.toString())}</p>
  </body>
</html>
""");
  return resp;
}

Exception handlers can be attached to the pipelines and the server.

A hierarchy determines which exception handler is invoked. If an exception occurs inside a request handler method (and has not been caught and processed within the handler) it is passed to the exception handler attached to the pipeline: the pipeline with the rule that invoked the request handler method. If no exception handler was attached to the pipeline, the exception handler attached to the server is used. If no exception handler was attached to the server, a default exception handler is used.

The hierarchy is also used if an exception handler itself throws an exception. (Though, hopefully, exception handlers will not throw an exception). In that situation, a ExceptionHandlerException is thrown.

4.1. Standard exceptions

The framework throws exceptions that are also processed by the same exception handling hierarchy.

The NotFoundException is thrown when a matching rule is not found. The exception handler should produce a "page not found" error page with a HTTP response status of either HttpStatus.NOT_FOUND or HttpStatus.METHOD_NOT_ALLOWED.

Other exceptions defined in the package are subclasses of WoomeraException.

5. Responses

The request handlers and exception handlers must return a Future that returns a Response object. The Response class is an abstract class and three subclasses of it have been defined in the package:

  • ResponseBuffered
  • ResponseStream
  • ResponseRedirect

5.1. ResponseBuffered

This is used to write the contents of the response into a buffer, which is used to create the HTTP response after the request hander returns.

The HTTP response is only created after the request handler finishes. If an error occurs while generating the response, the partially created ResponseBuffered object can be discarded and a new response created. The new response can be created in the response handler or in an exception handler. The new response can show an error page, instead of trying to output an error message at the end of a partially generated page.

5.2. ResponseRedirect

This is used to generate a HTTP redirect, which tells the client to go to a different URL.

5.3. ResponseStream

This is used to produce the contents of the response from a stream.

5.4. Common features

With all three types of responses, the application can:

  • Set the HTTP status code;
  • Create HTTP headers; and/or
  • Create or delete cookies.

5.5. Static file response

The package includes a request handler for serving up files and directories from the local disk. It can be used to serve static files for all or some of the Web server (for example, the images and stylesheets).

See the API documentation for the StaticFiles class.

6. Sessions

The framework provides a mechanism to manage sessions. HTTP is a stateless protocol, but sessions have been added to support the tracking of state.

A session can be created and attached to a HTTP request. That session will be attached to subsequent Request objects. The framework handles the preserving and restoration of the session using either session cookies or URL rewriting. The application can terminate a session, or they will automatically terminate after a nominated timeout period after they were last used.

7. References

Changelog

3.1.1

  • Fixed problem with publishing documentation on pub.dartlang.org.

3.0.0

  • Updated the upper bound of the SDK constraint to <3.0.0.
  • Changed names to use new Dart 2 names.

2.2.1

  • This version runs under Dart 1.
  • Updated dependencies to allow for Dart 2 compatible versions to be used.

2.2.0

  • Changed RequestFactory to return FutureOr<Request> instead of Request.
  • Added release method on Request class to perform cleanup operations.
  • Deprecated requestFactory: renamed to requestCreator.

2.1.1

  • Included Length, Last-Modified, and Date HTTP headers for StaticFiles.

2.1.0

  • Added ability to retrieve the number of active sessions.
  • Added access to creation time for sessions.
  • Added expiry time for sessions.
  • Stopping a server also terminates any sessions.

2.0.0

  • Code made sound to support Dart strong mode.
  • Removed arbitrary properties from Request and Session: use subtypes instead.
  • Changed default bindAddress from LOOPBACK_IP_V6 to LOOPBACK_IP_V4.
  • Added convenience methods for registering PUT, PATCH, DELETE and HEAD handlers.
  • Added coverage tests.

1.0.5

  • Upgraded version dependency on uuid package.

1.0.4

2016-09-29

  • Fixed bug with parallel processing of HTTP requests.

1.0.3

2016-05-11

  • Fixed potential issue with URL rewriting in Chrome with GET forms.

1.0.2

2016-05-06

  • Improved exception catching in request processing loop.

1.0.1

2016-04-28

  • Fixed homepage URL.

1.0.0

2016-04-23

  • Initial release.

example/example.dart

/// Woomera demonstration Web Server.
///
/// This program runs a Web server to demonstrate the features of the Woomera
/// framework.
///
/// This program runs a single HTTP Web server (on port 1024), and has defined
/// two pipelines for processing the HTTP requests.
///
/// Copyright (c) 2016, Hoylen Sue. All rights reserved. Use of this source code
/// is governed by a BSD-style license that can be found in the LICENSE file.
//----------------------------------------------------------------

import 'dart:async';
import 'dart:convert' show json;
import 'dart:io'
    show
        ContentType,
        Cookie,
        HttpStatus,
        InternetAddress,
        FileSystemEntity,
        Platform;

import 'package:logging/logging.dart';

import 'package:woomera/woomera.dart';

//================================================================
// Globals

/// Application logger.

Logger mainLog = new Logger("main");

// The Web server and pipelines.
//
// Normally these can be local variables, but they are made global
// so some of the handler functions can access them. They are only
// manipulated for testing purposes: a normal Web application would
// usually not need to manipulate them after they have been setup.

/// Web server
///
Server webServer;

/// First pipeline
///
ServerPipeline p1;

/// Second pipeline
///
ServerPipeline p2;

//================================================================
/// Session for login.
///
/// Extends the [Session] class with the time the login started
/// and the user name.
///
class LoginSession extends Session {
  /// When the login started
  DateTime when;

  /// The name of the user logged in.
  ///
  /// Can be null.
  String name;

  /// Constructor for a login session.
  ///
  LoginSession(Server server, Duration timeout, this.when, [this.name = null])
      : super(server, timeout);
}

//================================================================
// Handlers
//
// These handlers are used in the rules that are registered in the pipelines
// (see the [main] method at the end this file).

//----------------------------------------------------------------
/// Common HTML code for the "home" button that appears on many pages.

String homeButton(Request req) =>
    "<p><a href='${req.rewriteUrl("~/")}' style='font-size: large; text-decoration: none;'>&#x21A9;</a></p>";

//----------------------------------------------------------------
/// Home page
///
/// Main page for the demonstration Web server.

Future<Response> homePage(Request req) async {
  final resp = new ResponseBuffered(ContentType.HTML)
    ..write("""
<!doctype html>
<html>
<head>
  <title>Woomera demo</title>
  <link rel="stylesheet" href="diskfiles/style/site.css">
</head>

<body>
  <header>
    <h1>Woomera demonstration</h1>
  </header>

  <div class="content">

  <div class="section">
    <h2>Request parameters</h2>

    <p>Three types of parameters can be passed to handlers.</p>

    <table class="main">
      <tbody>
        <tr>
          <td>Path parameters</td>
          <td>
            <p>Matching to pattern with <a href="/must/all/match">no path parameters</a></p>
            <p>Matching <a href="/two/hello/world">/two/hello/world</a> to <code>/two/:first/:second</code></p>
            <p>Matching <a href="/wildcard1/a/b/c">/wildcard1/a/b/c</a> to <code>/wildcard1/*</code></p>
          </td>
        <tr>
          <td>Query parameters</td>
          <td>
            <p><a href="/test?foo=bar">?foo=bar</a></p>
            <p><a href="/test?greeting=hello&name=world">?greeting=hello&name=world</a></p>
            <p><a href="/test?p=query&p=parameters&p=can+be&p=repeated">?p=query&p=parameters&p=can+be&p=repeated</a></p>
            <p><a href="/test?note=Unicode+is+supported&value=안녕하세요+세계&value=hello+world">?note=Unicode+is+supported&value=안녕하세요+세계&value=hello+world</a></p>
          </td>
        </tr>
        <tr>
          <td>POST parameters</td>
          <td>
            <p>
            <form method="POST" action="/test">
            <input type="text" name="foo"/>
            &nbsp;
            <input type="radio" name="r" id="r-A" value="A"/><label for="r-A">A</label>
            <input type="radio" name="r" id="r-B" value="B"/><label for="r-B">B</label>
            <input type="radio" name="r" id="r-C" value="C"/><label for="r-C">C</label>
            &nbsp;
            <input type="checkbox" id="chk-a" name="chk-a" value="α"/><label for="chk-a">α</label>
            <input type="checkbox" id="chk-b" name="chk-b" value="β"/><label for="chk-b">β</label>
            <input type="checkbox" id="chk-c" name="chk-c" value="γ"/><label for="chk-c">γ</label>
            &nbsp;
            <input type="submit" value="Submit"/>
            </form>
            </p>
          </td>
        </tr>
      </tbody>
    </table>
  </div>

  <div class="section">
    <h2>Pattern matching of path parameters</h2>
  
    <p>More examples of path parameter matching.</p>
  
    <table class="main">
      <thead>
        <tr>
          <th>Pattern</th>
          <th>Example matches</th>
          <th>Example non-matches</th>
        <tr>
        </thead>
      <tbody>
        <tr>
          <td><code>/must/all/match</code></td>
          <td>
            <p><a href="${req.rewriteUrl("~/must/all/match")}">/must/all/match</a></p>
          </td>
          <td>
            <p><a href="/must">/must</a> missing a component</p>
            <p><a href="/must/all">/must/all</a> missing a component</p>
            <p><a href="/must/match">/must/match</a> missing a component</p>
            <p><a href="/all/match">/all/match</a> missing a component</p>
            <p><a href="/doesnt/all/match">/doesnt/all/match</a> component does not match</p>
            <p><a href="/must/some/match">/must/some/match</a> component does not match</p>
            <p><a href="/must/all/Match">/must/all/Match</a> component does not match (case sensitivity matters)</p>
            <p><a href="/must/all/match/exactly">/must/all/match/exactly</a> too many components</p>
            <p><a href="/must/all/match/">/must/all/match/</a> too many components (trailing slash produces another component)</p>
          </td>
        </tr>
        <tr>
          <td><code>/one/:first</code></td>
          <td>
            <p><a href="/one/alpha">/one/alpha</a></p>
            <p><a href="/one/">/one/</a></p>
          </td>
          <td>
            <p><a href="/one">/one</a> missing parameter</p>
            <p><a href="/one/alpha/beta">/one/alpha/beta</a> too many parameters</p>
          </td>
        </tr>
        <tr>
          <td><code>/one/:first/:second</code></td>
          <td>
            <p><a href="/two/alpha/beta">/two/alpha/beta</a></p>
            <p><a href="/two/alpha/">/two/alpha/</a></p>
            <p><a href="/two//">/two//</a></p>
            <p><a href="/two/你好/世界">/two/你好/世界</a></p>
          </td>
          <td>
            <p><a href="/two/alpha">/two/alpha</a> insufficient parameters</p>
            <p><a href="/two/alpha/beta/gamma">/two/alpha/beta/gamma</a> too many parameters</p>
          </td>
        </tr>
        <tr>
          <td><code>/one/:first/:second/:third</code></td>
          <td>
            <p><a href="/three/alpha/beta/gamma">/three/alpha/beta/gamma</a></p>
            <p><a href="/three/alpha/beta/">/three/alpha/beta/</a></p>
            <p><a href="/three/alpha//">/three/alpha//</a></p>
            <p><a href="/three///">/three///</a></p>
            <p><a href="/three//beta/gamma">/three//beta/gamma</a></p>
            <p><a href="/three/alpha//gamma">/three/alpha//gamma</a></p>
            <p><a href="/three//beta/">/three//beta/</a></p>
            <p><a href="/three///gamma">/three///gamma</a></p>
          </td>
        </tr>
        <tr>
          <td><code>/double/:name/:name</code></td>
          <td><a href="/double/alpha/beta">/double/alpha/beta</a></td>
        </tr>
        <tr>
          <td><code>/triple/:name/:name/:name</code></td>
          <td><a href="/triple/alpha/beta/gamma">/triple/alpha/beta/gamma</a></td>
        </tr>
        <tr>
          <td><code>/wildcard1/*</code></td>
          <td>
            <p><a href="/wildcard1/">/wildcard1/</a></p>
            <p><a href="/wildcard1/alpha">/wildcard1/alpha</a></p>
            <p><a href="/wildcard1/alpha/beta">/wildcard1/alpha/beta</a></p>
            <p><a href="/wildcard1/alpha/beta/gamma">/wildcard1/alpha/beta/gamma</a></p>
          </td>
        </tr>
  
        <tr>
          <td><code>/wildcard2/*/foo/bar</code></td>
          <td>
            <p><a href="/wildcard2/alpha/beta/gamma/foo/bar">/wildcard2/alpha/beta/gamma/foo/bar</a></p>
          </td>
          <td>
            <p><a href="/wildcard2/">/wildcard2/</a></p>
            <p><a href="/wildcard2/alpha">/wildcard2/foo/bar</a></p>
          </td>
        </tr>
  
        <tr>
          <td><code>/wildcard3/*/*</code></td>
          <td>
            <p><a href="/wildcard3/alpha/beta">/wildcard3/alpha/beta</a></p>
            <p><a href="/wildcard3/alpha/beta/gamma">/wildcard3/alpha/beta/gamma</a></p>
            <p><a href="/wildcard3/alpha/beta/gamma/delta">/wildcard3/alpha/beta/gamma/delta</a></p>
          </td>
        </tr>
  
        <tr>
          <td><code>/wildcard4/*/foo/bar/*/baz</code></td>
          <td>
            <p><a href="/wildcard4/alpha/beta/gamma//foo/bar/delta/baz">/wildcard4/alpha/beta/gamma/foo/bar/delta/baz</a></p>
          </td>
        </tr>
  """)

    // Static files and directories

    ..write("""
        <tr>
          <td colspan="3"><a name="staticFiles"><h3>Static files and directories from disk</h3></a></td>
        </tr>

        <tr>
          <td>No end-slash could be directory: yes;<br/>Directory listing allowed: yes</td>
          <td>
            <p><a href="/diskfiles/dir-with-index/">/dir-with-index/index.html</a></p>
            <p><a href="/diskfiles/dir-with-index/">/dir-with-index/</a></p>
            <p><a href="/diskfiles/dir-with-index">/dir-with-index</a></p>
            <p><a href="/diskfiles/dir-no-index/">/dir-no-index/</a></p>
            <p><a href="/diskfiles/dir-no-index">/dir-no-index</a></p>
          </td>
          <td>
            <p><a href="/diskfiles/no-such-file.html">/no-such-file.html</a></p>
          </td>
        </tr>

        <tr>
          <td>No end-slash could be directory: yes;<br/>Directory listing allowed: no</td>
          <td>
             <p><a href="/diskfilesDir1List0/dir-with-index/">/dir-with-index/index.html</a></p>
             <p><a href="/diskfilesDir1List0/dir-with-index/">/dir-with-index/</a></p>
             <p><a href="/diskfilesDir1List0/dir-with-index">/dir-with-index</a></p>
          </td>
          <td>
            <p><a href="/diskfilesDir1List0/no-such-file.html">/no-such-file.html</a></p>
            <p><a href="/diskfilesDir1List0/dir-no-index">/dir-no-index</a></p>
            <p><a href="/diskfilesDir1List0/dir-no-index/">/dir-no-index/</a></p>
          </td>
        </tr>

        <tr>
          <td>No end-slash could be directory: no;<br/>Directory listing allowed: yes</td>
          <td>
            <p><a href="/diskfilesDir0List1/dir-with-index/">/dir-with-index/index.html</a></p>
            <p><a href="/diskfilesDir0List1/dir-with-index/">/dir-with-index/</a></p>
            <p><a href="/diskfilesDir0List1/dir-no-index/">/dir-no-index/</a></p>
          </td>
          <td>
            <p><a href="/diskfilesDir0List1/no-such-file.html">/no-such-file.html</a></p>
            <p><a href="/diskfilesDir0List1/dir-with-index">/dir-with-index</a></p>
            <p><a href="/diskfilesDir0List1/dir-no-index">/dir-no-index</a></p>
          </td>
        </tr>

        <tr>
          <td>No end-slash could be directory: no;<br/>Directory listing allowed: no</td>
          <td>
            <p><a href="/diskfilesDir0List0/dir-with-index/">/dir-with-index/index.html</a></p>
            <p><a href="/diskfilesDir1List0/dir-with-index/">/dir-with-index/</a></p>
          </td>
          <td>
            <p><a href="/diskfilesDir0List0/no-such-file.html">/no-such-file.html</a></p>
            <p><a href="/diskfilesDir0List0/dir-with-index">/dir-with-index</a></p>
            <p><a href="/diskfilesDir0List0/dir-no-index/">/dir-no-index/</a></p>
            <p><a href="/diskfilesDir0List0/dir-no-index">/dir-no-index</a></p>
          </td>
        </tr>

        <tr>
          <td>Different MIME types</td>
          <td>
            <p><a href="/diskfiles/test.html">/test.html</a></p>
            <p><a href="/diskfiles/test.jpg">/test.jpg</a></p>
            <p><a href="/diskfiles/test.png">/test.png</a></p>
            <p><a href="/diskfiles/test.txt">/test.txt</a></p>
            <p><a href="/diskfiles/test.xml">/test.xml</a></p>
            <p><a href="/diskfiles/test.dat">/test.dat</a></p>
          </td>
        </tr>

      </tbody>
    </table>
  </div>
""");

  // Exception handling

  final eh1checked =
      (webServer.exceptionHandler != null) ? "checked='checked'" : "";
  final eh2checked = (p1.exceptionHandler != null) ? "checked='checked'" : "";
  final eh3checked = (p2.exceptionHandler != null) ? "checked='checked'" : "";

  resp
    ..write("""
  <div class="section">
    <h2>Exception handling</h2>

    <p>If a handler throws an exception, the exception is passed to an exception
    hander that was registered (either registered with the pipeline or with the
    server).</p>

    <ul>
      <li><a href="/throw/IntegerDivisionByZeroException">IntegerDivisionByZeroException</a></li>
      <li><a href="/throw/FormatException">FormatException</a> thrown.</li>
      <li><a href="/throw/StateError">StateError</a> thrown (after 3 seconds).</li>
    </ul>

    <p>Change which exception handlers have been set:</p>

    <form method="POST" action="/system/exceptionHandler">
      <input type="checkbox" id="eh0" name="eh0" value="on" $eh1checked/><label for="eh0">Server-level</label>
      <input type="checkbox" id="eh1" name="eh1" value="on" $eh2checked/><label for="eh1">Pipeline 1</label>
      <input type="checkbox" id="eh2" name="eh2" value="on" $eh3checked/><label for="eh2">Pipeline 2</label>
      &nbsp;
      <input type="submit" value="Set Exception Handlers"/>
    </form>
  </div>
""")

    // Sessions

    ..write("""
  <div class="section">
    <h2>Sessions</h2>
    <p>Sessions can be used to maintain state between HTTP requests. It can use
    either session cookies or URL rewriting to remember the current session.</p>
    """);

  final requestSession = req.session;
  if (requestSession == null) {
    // Not logged in
    resp.write("""
  <ul>
    <li><a href=\"${req.rewriteUrl("~/session/loginWithCookies")}\">Login using
        cookies to preserve the session</a> (if the browser uses them)</li>
    <li><a href=\"${req.rewriteUrl("~/session/login")}\">Login without cookies</a></li>
  </ul>
  """);
  } else if (requestSession is LoginSession) {
    // Logged in
    assert(requestSession != null);
    resp.write("""
  <ul>
    <li><a href=\"${req.rewriteUrl("~/session/info")}\">Session information page</a></li>
    <li><a href=\"${req.rewriteUrl("~/session/logout")}\">Logout</a></li>
  </ul>
  <p style="font-size: smaller">Logged in at: ${requestSession.when}</p>
  """);
  }

  resp
    ..write("</div>")

    // Stream response

    ..write("""
    <div class="section">
    <h2>Stream test</h2>

    <p>Responses can be buffered and the HTTP response produced after the
    handler has completed, or progressively produced in the handler
    as a stream.</p>

    <ul>
      <li><a href="/streamTest">Basic stream response</a></li>
      <li><a href="/streamTest?seconds=1">Stream response with delays</a></li>
    </ul>

  </div>
  """)

    // End of content div, and footer

    ..write("""</div>

  </div>

  <footer>
    <p><a href="https://pub.dartlang.org/packages/woomera">Woomera Dart Package</a></p>
  </footer>
</body>
</html>
""");

  return resp;
}

//----------------------------------------------------------------
/// Handler for post operation
///
Future<Response> handleTestPost(Request req) async {
  assert(req.request.method == "POST");

  mainLog.fine("[${req.id}] Test POST");
/*
  for (var key in req.params.keys()) {
    var values = req.params.mvalues(key, raw: true);
    if (values.length == 1) {
      // Single value
      print("${key}=\"${values[0]}\"");
    } else {
      // Multi-valued
      print("${key}= [");
      var index = 0;
      for (var value in values) {
        print("[${++index}]=\"${value}\"");
      }
      print("]");
    }
  }
*/

  final resp = new ResponseBuffered(ContentType.TEXT)..write("Test post\n");
  return resp;
}

//----------------------------------------------------------------
/// Exception handler
///
Future<Response> handleExceptionHandlers(Request req) async {
  final eh0 = req.postParams["eh0"] == "on";
  final eh1 = req.postParams["eh1"] == "on";
  final eh2 = req.postParams["eh2"] == "on";

  webServer.exceptionHandler = (eh0) ? exceptionHandlerOnServer : null;
  p1.exceptionHandler = (eh1) ? exceptionHandlerOnPipe1 : null;
  p2.exceptionHandler = (eh2) ? exceptionHandlerOnPipe2 : null;

  mainLog.fine("[${req.id}] setting exception handlers");
  final resp = new ResponseBuffered(ContentType.HTML)..write("""
<html>
<head></head>
<body>
<h1>System setup: Exception handlers</h1>

<ul>
  <li>Server exception handler: $eh0</li>
  <li>Pipe 1 exception handler: $eh1</li>
  <li>Pipe 2 exception handler: $eh2</li>
</ul>

${homeButton(req)}
""");
  return resp;
}

//----------------------------------------------------------------
/// Handler to stop the Web server.
///
Future<Response> handleStop(Request req) async {
  await webServer.stop();
  mainLog.fine("[${req.id}] stopped");
  final resp = new ResponseBuffered(ContentType.TEXT)
    ..write("Web server has been stopped\n");
  return resp;
}

//----------------------------------------------------------------
/// Handler that throws exceptions.
///
Future<Response> handleThrow(Request req) async {
  mainLog.fine("[${req.id}] exception test");

  final type = req.pathParams["name"];

  switch (type) {
    case "":
      break;
    case "0":
    case "IntegerDivisionByZeroException":
      final _ = 42 ~/ 0;
      break;

    case "1":
    case "FormatException":
      final _ = int.parse("16C");
      break;

    case "2":
    case "StateError":
      return await oldStyleFuture(throwException: true);

    default:
      throw new ArgumentError("Unknown name: $type");
  }

  final resp = new ResponseBuffered(ContentType.HTML)
    ..status = HttpStatus.NOT_ACCEPTABLE
    ..write("""
<html>
<head><title>Exception test</title></head>
<body>
<h1>Exception test</h1>
<p>The handler for this page can throw different exceptions, depending on
what the path parameter is:</p>
<dl>
  <dt>blank</dt>
    <dd>No exception is thrown. Shows this page.</dd>
  <dt>0</dt>
    <dd>Throws an <code>IntegerDivisionByZeroException</code></dd>
  <dt>2</dt>
    <dd>Throws a <code>FormatError</code></dt>
  <dt>1</dt>
    <dd>Throws a <code>StateError</code></dt>
  <dt>Other values</dt>
    <dd>Throws a <code>String</code> of that value.</dd>
</dl>

<p>If no exception handlers have been provided, the exception is logged
(with "SEVERE" logging level) and an uninformative error page is
returned. It is deliberately uninformative so no internal information
can be accidently leaked.</body>
</html>
""");
  return resp;
}

/// Method that throws an exception some time in the future.
///
Future<ResponseBuffered> oldStyleFuture({bool throwException: false}) {
  const duration = const Duration(seconds: 3);

  final c = new Completer<ResponseBuffered>();

  final _ = new Timer(duration, () {
    if (throwException) {
      // This exception is thrown from a function that is not using the new
      // async/await syntax. This means it won't be caught by the try/catch
      // mechanism. The framework will catch these using zones.
      throw new StateError(new DateTime.now().toString());
    }

    final resp = new ResponseBuffered(ContentType.TEXT)
      ..status = HttpStatus.NOT_ACCEPTABLE
      ..write("""This worked, but it should not have.""");
    c.complete(resp);
  });

  return c.future;
}

//----------------------------------------------------------------
/// Stream handler
///
/// This is an example of using a [ResponseStream] to progressively
/// create the response.

Future<Response> streamTest(Request req) async {
  // Get parameters

  final numIterations = 10;

  var secs = 0;
  if (req.queryParams["seconds"].isNotEmpty) {
    secs = int.parse(req.queryParams["seconds"]);
  }

  // Produce the stream response

  final resp = new ResponseStream(ContentType.TEXT)..status = HttpStatus.OK;
  await resp.addStream(req, _streamSource(req, numIterations, secs));

  return resp;
}

// The stream that produces the data making up the response.
//
// It produces a stream of bytes (List<int>) that make up the contents of
// the response.

Stream<List<int>> _streamSource(Request req, int iterations, int secs) async* {
  final delay = new Duration(seconds: secs);

  yield "Stream of $iterations items (delay: $secs seconds)\n".codeUnits;

  yield "Started: ${new DateTime.now()}\n".codeUnits;

  for (var x = 1; x <= iterations; x++) {
    final completer = new Completer<int>();
    new Timer(delay, () => completer.complete(0));
    await completer.future;

    yield "Item $x\n".codeUnits;
  }
  yield "Finished: ${new DateTime.now()}\n".codeUnits;
}

//----------------------------------------------------------------
/// Handler that returns JSON in the response.
///
Future<Response> handleJson(Request req) async {
  final data = {'name': "John Citizen", 'address': "foo"};

  // response.headers.contentType = ContentType.json
  print(json.encode(data));
  // json.decode(str);

  final resp = new ResponseBuffered(ContentType.TEXT)..write("JSON test");
  return resp;
}

//================================================================
// Exception handlers

//----------------------------------------------------------------
/// Exception handler for the server.
///
/// This exception handler is attached to the [Server] and will
/// be invoked if an exception is raised outside the context
/// of the pipelines (or the pipeline did not process any exceptions
/// raised inside their context).

Future<Response> exceptionHandlerOnServer(
    Request req, Object exception, StackTrace st) {
  assert(req != null);
  return _exceptionHandler(req, exception, st, "server");
}

//----------------------------------------------------------------
/// Exception handler for pipeline1.
///
/// This exception handler is attached to the first pipeline.

Future<Response> exceptionHandlerOnPipe1(
    Request req, Object exception, StackTrace st) async {
  if (exception is StateError) {
    return null;
  }
  return _exceptionHandler(req, exception, st, "pipeline1");
}

//----------------------------------------------------------------
/// Exception handler for pipeline2.
///
/// This exception handler is attached to the second pipeline.

Future<Response> exceptionHandlerOnPipe2(
    Request req, Object exception, StackTrace st) async {
  if (exception is StateError) {
    return null;
  }
  return _exceptionHandler(req, exception, st, "pipeline2");
}

//----------------------------------------------------------------
// Common method used to implement the above exception handlers.

Future<Response> _exceptionHandler(
    Request req, Object exception, StackTrace st, String who) async {
  // Create a response

  final resp = new ResponseBuffered(ContentType.HTML);

  // Set the status depending on the type of exception

  if (exception is NotFoundException) {
    resp.status = (exception.found == NotFoundException.foundNothing)
        ? HttpStatus.METHOD_NOT_ALLOWED
        : HttpStatus.NOT_FOUND;
  } else {
    resp.status = HttpStatus.INTERNAL_SERVER_ERROR;
  }

  // The body of the response

  resp.write("""
<html>
<head>
  <title>Exception</title>
</head>
<body>
<h1 style="color: red">Exception thrown</h1>

An exception was thrown and was handled by the <strong>$who</strong> exception handler.

<h2>Exception</h2>

<p>Exception object type: <code>${exception.runtimeType}</code></p>
<p>String representation of object: <strong>$exception</strong></p>
""");

  if (st != null) {
    resp.write("""
<h2>Stack trace</h2>
<pre>
$st
</pre>
    """);
  }
  resp.write("""
${homeButton(req)}
</body>
</html>
""");

  return resp;
}

//================================================================
// Session

const String _testCookieName = "browser-test";

Future<Response> _handleLoginWithCookies(Request req) async {
  final testCookie = new Cookie(_testCookieName, "cookies_work!")
    ..path = req.server.basePath
    ..httpOnly = true;

  final resp = new ResponseBuffered(ContentType.HTML)
    ..cookieAdd(testCookie)
    ..write("""
<html>
<head></head>
<body>
<h1>Determining if the browser uses cookies</h1>

<p>An attempt has been done to set a test cookie. If the browser
presents the cookie when when the login page is visited, it will know
the browser supports cookies and will automatically use cookies to
preserve the session. If no cookies are presented to the login page,
it will not use cookies and instead use URL rewriting to preserve the
session.  The login page will not be presented the cookie: if the
browser does not support cookies, if the browser supports cookies but
they have been disabled, or if the login page is visited directly
(without going via this page).</p>

<p>Now please visit the
<a href="${req.rewriteUrl("~/session/login")}">login page</a>.</p>

</body>
</html>
""");

  return resp;
}

//----------------------------------------------------------------

Future<Response> _handleLogin(Request req) async {
  const keepAlive = const Duration(minutes: 1);

  req.session = new LoginSession(webServer, keepAlive, new DateTime.now());

  final resp = new ResponseBuffered(ContentType.HTML)
    ..cookieDelete(_testCookieName, req.server.basePath)
    ..write("""
<html>
<head></head>
<body>
<h1>Session: logged in</h1>

<p>You have logged in.</p>

<p>The session will remain alive for ${keepAlive.inSeconds} seconds, after the
last HTTP request was received for the session.</p>

<p><a href="${req.rewriteUrl("~/session/info")}">Session information page</a></p>

${homeButton(req)}
</body>
</html>
""");
  return resp;
}

//----------------------------------------------------------------

Future<Response> _handleLogout(Request req) async {
  final resp = new ResponseBuffered(ContentType.HTML)..write("""
<html>
<head></head>
<body>
<h1>Session: logout</h1>
""");

  if (req.session != null) {
    await req.session
        .terminate(); // terminate the session (also removes the timer)
    req.session = null; // clear the session so it is no longer preserved

    resp.write("<p>You have been logged out.</p>");
  } else {
    resp.write("<p>Error: not logged in: you should not see this page.</p>");
  }

  resp.write("""
${homeButton(req)}
</body>
</html>
""");
  return resp;
}

//----------------------------------------------------------------

Future<Response> _handleSessionInfoPage(Request req) async {
  final resp = new ResponseBuffered(ContentType.HTML)..write("""
<html>
<head></head>
<body>
<h1>Session information</h1>
""");

  final requestSession = req.session;
  if (requestSession is LoginSession) {
    assert(requestSession != null);

    final duration = new DateTime.now().difference(requestSession.when);

    if (requestSession.name != null) {
      resp.write(
          "<p>Welcome <strong>${HEsc.text(requestSession.name)}</strong>.</p>");
    }
    resp.write("""
<p>Logged in at ${requestSession.when}.
You have been logged in for over ${duration.inSeconds} seconds.</p>

<h2>Session preservation across GET requests</h2>

<p>If using cookies, the session is preserved using a session cookie.
Otherwise URL rewriting is used to preserve the session for GET requests,
and the following link (back to this page) will have a session query parameters</p>

<ul>
  <li><a href="${req.rewriteUrl("~/session/info")}">Session info page</a></li>
  <li><a href="${req.rewriteUrl("~/session/info?foo=bar&foo=baz&abc=xyz")}">With other query parameters</a></li>
</ul>

<p>Note: any session query parameters are stripped out so the application never
sees them. The handler that processed this request saw
""");
    if (req.queryParams.isEmpty) {
      resp.write("no query parameters.");
    } else {
      resp.write("these query parameters: ${req.queryParams}");
    }

    resp.write("""
<h2>Session preserved across POST requests</h2>

<p>If using cookies, the session is preserved using a session cookie.
Otherwise the session needs to be preserved using a parameter.</p>

<p>For a POST request, typically this is done by rewriting the form's action URL.
So the POST request actually has both query parameters and POST parameters
(though the application never sees this, because the session parameter is
stripped out after processing it).</p>

<form method="POST" action="${req.rewriteUrl("~/session/set-name")}">
  <label for="n">Name:</label>
  <input type="text" name="name" id="n"/>
  <input type="submit" value="Set name"/>
</form>

<p>Although it is possible to preserve the session using a hidden form
parameter, that is not recommended because rewriting a URL also incorporates
the server's basePath in the URL. If the URL is not rewritten, there is a risk
that changes to the basePath are not incorporated in the URL and the application
breaks.</p>

<p>Do not both rewrite the form's action URL and include the hidden form field.
That will be detected as multiple session IDs (even though they are the
same value) and (for security and consistent behaviour) they will all be ignored
and the session will be lost.</p>

<p>The session can also be lost if the cookies are cleared from the browser
(when using cookies) or a link which has not been rewritten is followed
(when not using cookies). Any external link will not be rewritten, so the user
must stay within the application to preserve the session, when cookies are
not used. A session will also be lost if/when it times out.</p>

""");
  } else {
    resp.write("<p>No session.</p>");
  }

  resp.write("""
${homeButton(req)}
</body>
</html>
""");
  return resp;
}

//----------------------------------------------------------------

Future<Response> _handleSessionSetName(Request req) async {
  final resp = new ResponseBuffered(ContentType.HTML)..write("""
<html>
<head></head>
<body>
<h1>Session: name set</h1>
""");

  final requestSession = req.session;
  if (requestSession is LoginSession) {
    assert(requestSession != null);
    requestSession.name = req.postParams["name"];

    if (requestSession.name.isNotEmpty) {
      resp.write(
          "<p>Your name has been set to \"${HEsc.text(requestSession.name)}\".</p>");
    } else {
      resp.write("<p>Your name has been cleared.</p>");
    }
    resp.write(
        "<p>Return to the <a href=\"${req.rewriteUrl("~/session/info")}\">session info page</a>.</p>");
  } else {
    resp.write("<p>Error: not logged in: you should not see this page.</p>");
  }

  resp.write("""
${homeButton(req)}
</body>
</html>
""");
  return resp;
}

//================================================================
// Main

//----------------------------------------------------------------
// Set up logging
//
// Change this to the level and type of logging desired.

void _loggingSetup() {
  hierarchicalLoggingEnabled = true;
  Logger.root.onRecord.listen((rec) {
    print('${rec.time}: ${rec.loggerName}: ${rec.level.name}: ${rec.message}');
  });

  Logger.root.level = Level.OFF;

  final commonLevel = Level.INFO;

  new Logger("main").level = commonLevel;
  new Logger("woomera.server").level = commonLevel;
  new Logger("woomera.request").level = Level.FINE;
  new Logger("woomera.request.header").level = commonLevel;
  new Logger("woomera.request.param").level = commonLevel;
  new Logger("woomera.response").level = commonLevel;
  new Logger("woomera.session").level = commonLevel;
}

//----------------------------------------------------------------

Server _serverSetup() {
  //--------
  // Create a new Web server
  //
  // The bind address is setup to listen to any incoming connection from any IP
  // address (IPv4 or IPv6). If this is not done, by default it only listens
  // on the IPv4 loopback interface, which is good for deployment behind a
  // reverse Web proxy, but might be restrictive for testing.
  //
  // Note: normally [webserver], [p1] and [p2] can be local variables. But in
  // this demo some of the HTTP requests will manipulate the server and
  // pipelines (which normally doesn't happen in ordinary applications) so
  // these are global variables so that the handler methods can access them.

  final port = 1024;

  webServer = new Server()
    ..bindAddress = InternetAddress.ANY_IP_V6
    ..v6Only = false // false = listen to any IPv4 and any IPv6 address
    ..bindPort = port
    ..exceptionHandler = exceptionHandlerOnServer; // set exception handler

  mainLog.info("Web server running on port $port");

  //--------
  // Setup the first pipeline
  //
  // Get the first pipeline and set the exception handler on it.

  p1 = webServer.pipelines.first

    // Set the first pipeline's exception handler
    //
    // If an exception is thrown, and is not processed by any pipeline's
    // exception

    ..exceptionHandler = exceptionHandlerOnPipe1;

  //--------
  // Setup the second pipeline
  //
  // A typical server usually only needs one pipeline, and all of the rules will
  // be defined in it. But to demonstrate the use of multiple pipelines, this
  // application has a second pipleline where most of the rules will be defined.
  //
  // Note: the other way to create multiple pipelines is to specify the number
  // of pipelines when the server is created:
  //     webServer = new Server(numberOfPipelines: 2);

  p2 = new ServerPipeline();
  webServer.pipelines.add(p2);

  // Set the second pipeline's exception handler
  //
  // If an exception is thrown by one of the handlers invoked from the second
  // pipeline, that exception will be passed to this exception handler.
  //
  // Applications can define exception handlers on the server, individual
  // pipelines, or a combination of both.

  p2
    ..exceptionHandler = exceptionHandlerOnPipe2

    // Set up the rules for the second pipeline. A rule consists of the HTTP
    // request method (e.g. GET or POST), a pattern to match against the request
    // path and the handler method.

    ..get("~/", homePage)
    ..get("~/must/all/match", debugHandler)
    ..post("~/must/all/match", debugHandler)
    ..get("~/one/:first", debugHandler)
    ..get("~/two/:first/:second", debugHandler)
    ..get("~/three/:first/:second/:third", debugHandler)
    ..post("~/one/:first", debugHandler)
    ..post("~/two/:first/:second", debugHandler)
    ..post("~/three/:first/:second/:third", debugHandler)
    ..get("~/double/:name/:name", debugHandler)
    ..get("~/triple/:name/:name/:name", debugHandler)
    ..post("~/double/:name/:name", debugHandler)
    ..post("~/triple/:name/:name/:name", debugHandler)
    ..get("~/wildcard1/*", debugHandler)
    ..get("~/wildcard2/*/foo/bar", debugHandler)
    ..get("~/wildcard3/*/*", debugHandler)
    ..get("~/wildcard4/*/foo/bar/*/baz", debugHandler)
    ..get("~/throw/:name", handleThrow) // tests exception handling

    ..get("~/test", debugHandler)
    ..post("~/test", debugHandler)
    ..get("~/streamTest", streamTest)
    ..get("~/session/login", _handleLogin)
    ..get("~/session/loginWithCookies", _handleLoginWithCookies)
    ..get("~/session/info", _handleSessionInfoPage)
    ..post("~/session/set-name", _handleSessionSetName)
    ..get("~/session/logout", _handleLogout);

  // Serve static files
  //
  // This rule uses the [StaticFiles] class to create a handler that serves
  // files and directories from a local directory. In this case, the "web"
  // directory underneath this project's directory. This project's directory
  // is found as the parent of the directory containing this program file.
  //
  // For this demo, only paths under "diskfiles" (i.e. pattern "~/diskfiles/*")
  // try to match static files. The pattern "~/*" could be used -- if it is made
  // the very last rule -- so it acts as a catch-all rule to server up the
  // file/directory if it exists, or to return a not-found exception if there is
  // no such file/directory. Though, if possible, it is better to use a more
  // restrictive pattern (e.g. "~/style/*" or "~/images/*").

  final dirContainingThisFile = FileSystemEntity.parentOf(Platform.script.path);
  final projectDir = FileSystemEntity.parentOf(dirContainingThisFile);

  //mainLog.info("projectDir: $projectDir");

  // Map paths without an end slash to a directory, allow directory listing

  final staticFiles = new StaticFiles("$projectDir/web",
      defaultFilenames: ["index.html", "index.htm"],
      allowFilePathsAsDirectories: true,
      allowDirectoryListing: true);

  p2.get("~/diskfiles/*", staticFiles.handler);

  // Do not try to treat paths without an end slash as directories, no listing

  final staticDir0List0 = new StaticFiles("$projectDir/web",
      defaultFilenames: ["index.html", "index.htm"],
      allowFilePathsAsDirectories: false,
      allowDirectoryListing: false);

  p2.get("~/diskfilesDir0List0/*", staticDir0List0.handler);

  // Maps paths without an end slash to a directory, no directory listing

  final staticDir1List0 = new StaticFiles("$projectDir/web",
      defaultFilenames: ["index.html", "index.htm"],
      allowFilePathsAsDirectories: true,
      allowDirectoryListing: false);

  p2.get("~/diskfilesDir1List0/*", staticDir1List0.handler);

  // Do not try to treat paths without an end slash as directories, allow listing

  final staticDir0List1 = new StaticFiles("$projectDir/web",
      defaultFilenames: ["index.html", "index.htm"],
      allowFilePathsAsDirectories: false,
      allowDirectoryListing: true);

  p2
    ..get("~/diskfilesDir0List1/*", staticDir0List1.handler)

    // Special handlers for demonstrating Woomera features

    ..post("~/system/exceptionHandler", handleExceptionHandlers)
    ..post("~/system/stop", handleStop);

  return webServer;
}

//----------------------------------------------------------------

Future main(List<String> args) async {
  _loggingSetup();

  final server = _serverSetup();

  mainLog.fine("started");

  await server.run();

  // The Future returned by the [run] method never gets completed, unless the
  // server's [stop] method is invoked. Most applications leave the web server
  // running "forever", so normally the server's [stop] method never gets
  // invoked.

  mainLog.fine("finished");
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  woomera: ^3.0.1

2. Install it

You can install packages from the command line:

with pub:


$ pub get

with Flutter:


$ flutter packages get

Alternatively, your editor might support pub get or flutter packages get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:woomera/woomera.dart';
  
Version Uploaded Documentation Archive
3.0.1 Aug 10, 2018 Go to the documentation of woomera 3.0.1 Download woomera 3.0.1 archive
3.0.0 Aug 10, 2018 Go to the documentation of woomera 3.0.0 Download woomera 3.0.0 archive
2.2.2 Aug 17, 2018 Go to the documentation of woomera 2.2.2 Download woomera 2.2.2 archive
2.2.1 Aug 9, 2018 Go to the documentation of woomera 2.2.1 Download woomera 2.2.1 archive
2.2.0 Jun 28, 2018 Go to the documentation of woomera 2.2.0 Download woomera 2.2.0 archive
2.1.1 Jan 24, 2018 Go to the documentation of woomera 2.1.1 Download woomera 2.1.1 archive
2.1.0 Dec 23, 2017 Go to the documentation of woomera 2.1.0 Download woomera 2.1.0 archive
2.0.0 Dec 11, 2017 Go to the documentation of woomera 2.0.0 Download woomera 2.0.0 archive
1.0.5 Jun 14, 2017 Go to the documentation of woomera 1.0.5 Download woomera 1.0.5 archive
1.0.4 Sep 29, 2016 Go to the documentation of woomera 1.0.4 Download woomera 1.0.4 archive

All 14 versions...

Popularity:
Describes how popular the package is relative to other packages. [more]
0
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
50
Learn more about scoring.

We analyzed this package on Sep 18, 2018, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.0.0
  • pana: 0.12.3

Platforms

Detected platforms: Flutter, other

Primary library: package:woomera/woomera.dart with components: io.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >1.9.9 <3.0.0
logging >=0.11.2 <0.11.4 0.11.3+2
uuid >=0.5.0 <1.1.0 1.0.3
Transitive dependencies
charcode 1.1.2
collection 1.14.11
convert 2.0.2
crypto 2.0.6
typed_data 1.1.6
Dev dependencies
test >=0.12.10 <1.4.0