
// // 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]) } }