logo

abstract class

webapp::Widget

sys::Obj
  web::Weblet
    webapp::Widget
//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   18 Mar 08  Andy Frank  Creation
//

using web

**
** Widget extends Weblet to provide functionality to aid in creating
** reusable UI components.
**
** See `docLib::Widget`
**
abstract class Widget : Weblet
{

//////////////////////////////////////////////////////////////////////////
// Weblet
//////////////////////////////////////////////////////////////////////////

  **
  ** Handle configuring the inital Widget pipeline.  To allow
  ** Widgets to be nested, we use two thread local Bufs to capture
  ** the output for the '<head>' and '<body>' tags separately.
  ** After the request has been serviced, we flush the Bufs to the
  ** actual output stream via `complete`.
  **
  ** If this method is called again (on any instance) after the
  ** initial call, it short-circuits and simply calls the default
  ** `web::Weblet.service` implemenation.
  **
  override Void service()
  {
    // if service has already been called on this thread
    // then just route to the default implemenation
    if (Thread.locals["webapp.widget.head"] != null)
    {
      super.service
      return
    }

    try
    {
      // measure perf
      start := Duration.now

      // create bufs
      head := Buf.make(1024)
      body := Buf.make(8192)

      // add locals
      Thread.locals["webapp.widget.head"] = WebOutStream.make(head.out)
      Thread.locals["webapp.widget.body"] = WebOutStream.make(body.out)

      // verify flash exists
      if (req.session["webapp.widget.flash"] == null)
        req.session["webapp.widget.flash"] = Str:Obj[:]

      // route to super then complete
      super.service
      complete(head, body)

      // clear flash on gets
      if (req.method == "GET") flash.clear

      // echo perf
      dur := Duration.now - start
      hsz := head.size
      bsz := body.size
      // WebAppStep.log.info("$req.uri (dur:${dur.toMillis}ms head:${hsz}b body:${bsz}b)")
    }
    finally
    {
      // remove locals
      Thread.locals.remove("webapp.widget.head")
      Thread.locals.remove("webapp.widget.body")
    }
  }

  **
  ** The default implemenation of 'doPost' attempts to route the
  ** request to a method on a Widget based on the 'action' query
  ** parameter:
  **
  **   http://foo.com/some/weblet?action=pod::Type.method
  **
  **  - If 'action' is not in the uri, return '404 Not Found'
  **
  **  - If 'action' does not map to a 'pod::Type.method', throw
  **    `sys::UnknownSlotErr`
  **
  **  - If 'action' maps to a valid type and method, then a
  **    new instance of that type is created, and the method
  **    is invoked on it.
  **
  override Void doPost()
  {
    action := req.uri.query["action"]

    // if no action, return 404
    if (action == null)
    {
      res.sendError(404)
      return
    }

    // find and invoke action
    method := Slot.findMethod(action, true)
    method.call1(method.parent == this.type ? this : method.parent.make)
  }

//////////////////////////////////////////////////////////////////////////
// Methods
//////////////////////////////////////////////////////////////////////////

  **
  ** The buffered WebOutStream for the <head> element.
  **
  once WebOutStream head()
  {
    return Thread.locals["webapp.widget.head"] as WebOutStream
  }

  **
  ** The buffered WebOutStream for the <body> element.
  **
  once WebOutStream body()
  {
    return Thread.locals["webapp.widget.body"] as WebOutStream
  }

  **
  ** A short-term storage that only exists for a single GET request,
  ** and is then automatically cleaned up.  It is convenient for
  ** passing notifications following a POST.
  **
  Str:Obj flash()
  {
    return req.session["webapp.widget.flash"] as Str:Obj
  }

  **
  ** Complete the current request by flushing the 'head' and 'body'
  ** Bufs to the actual response OutStream.  If the current request
  ** is a GET, this method is responsible for adding the appropriate
  ** markup to make the resulting HTML a valid page.
  **
  virtual Void complete(Buf head, Buf body)
  {
    // if the response is already committed, assume this is
    // an error or redirect, in which case, we don't need to
    // deal with the buffers
    if (res.isCommitted) return

    get := req.method == "GET"
    if (get)
    {
      // TODO - we need to get our charset without calling 'out'
      // which flushes the headers, so we can't set them after!
      charset := "UTF-8" //res.out.charset.name
      res.headers["Content-Type"] = "text/html; charset=$charset"
      res.headers["Content-Encoding"] = charset
      res.out.docType
      res.out.html
      res.out.head
      res.out.printLine("<meta http-equiv='Content-Type' content='text/html; charset=$charset'/>")
    }
    res.out.writeBuf(head.flip)
    if (get)
    {
      res.out.headEnd
      res.out.body
    }
    res.out.writeBuf(body.flip)
    if (get)
    {
      res.out.bodyEnd
      res.out.htmlEnd
    }
  }

  **
  ** Write out an opening '<form>' tag to submit to the given
  ** action.  If 'uri' is null, 'req.uri' is used for the base
  ** Uri to submit the form to.
  **
  **   <form method='post' action='${uri}?action=$action.qname'>
  **
  Void actionForm(Func action, Str attrs := null, Uri uri := null)
  {
    body.tag("form method='post' action='${actionUri(action,uri).encode}'", attrs)
  }

  **
  ** Return the 'uri' used to invoke the given action on the
  ** given uri. If 'uri' is null, 'req.uri' is used for the base
  ** Uri to submit the action to.  The 'action' func must map to
  ** a [Method]`sys::Method`.
  **
  Uri actionUri(Func action, Uri uri := null)
  {
    method := action.method
    if (method == null) throw Err.make("Must use a Method")
    if (uri == null) uri = req.uri
    return uri.plusQuery(["action":method.qname])
  }

}