PageRenderTime 13ms CodeModel.GetById 2ms app.highlight 5ms RepoModel.GetById 1ms app.codeStats 0ms

/src/fan/DraftMod.fan

https://bitbucket.org/afrankvt/draft/
Unknown | 261 lines | 224 code | 37 blank | 0 comment | 0 complexity | b3b00b3cf221d4b77ef5cd7c8e5afac9 MD5 | raw file
  1//
  2// Copyright (c) 2011, Andy Frank
  3// Licensed under the MIT License
  4//
  5// History:
  6//   14 May 2011  Andy Frank  Creation
  7//
  8
  9using concurrent
 10using util
 11using web
 12using webmod
 13
 14**
 15** DraftMod
 16**
 17abstract const class DraftMod : WebMod
 18{
 19  ** Constructor.
 20  new make()
 21  {
 22    router = Router { routes=[,] }
 23
 24    // check for pubDir prop
 25    dir := typeof.pod.config("pubDir")
 26    if (dir != null) pubDir = dir.toUri.toFile
 27  }
 28
 29  ** Router model.
 30  const Router router
 31
 32  **
 33  ** Directory to publish as public files under '/pub/' URI:
 34  **   pubDir := `/foo/bar/`
 35  **   /foo/bar/index.css     =>  `/pub/index.css`
 36  **   /foo/bar/img/logo.png  =>  `/pub/img/logo.png`
 37  **
 38  ** The pubDir may also be defined as a [config]`sys::Env#config`
 39  ** property in 'etc/draft/config.props`
 40  **
 41  const File? pubDir := null
 42
 43  **
 44  ** Directory to write log files to.  If left null, no logging
 45  ** will be performed.
 46  **
 47  const File? logDir := null
 48
 49  **
 50  ** Format of the web log records as a string of names.
 51  ** See [webmod]`webmod::pod-doc#log`
 52  **
 53  const Str logFields := "date time c-ip cs(X-Real-IP) cs-method " +
 54                         "cs-uri-stem cs-uri-query sc-status time-taken " +
 55                         "cs(User-Agent) cs(Referer) cs(Cookie)"
 56
 57  **
 58  ** Map of URI path names to sub-WebMods. Sub mods are checked
 59  ** for matching routes before we process our own routes.
 60  **
 61  // TODO: not sure how this works yet
 62  @NoDoc
 63  const Str:WebMod subMods := Str:WebMod[:]
 64
 65  ** Invoked prior to serviceing the current request.
 66  virtual Void onBeforeService(Str:Str args) {}
 67
 68  ** Invoked after serviceing the current request.
 69  virtual Void onAfterService(Str:Str args) {}
 70
 71  ** Service incoming request.
 72  override Void onService()
 73  {
 74    try
 75    {
 76      // set mod
 77      req.mod = this
 78
 79      // check for pub
 80      if (req.uri.path.first == "pub" && pubDir != null)
 81        { onServicePub; return }
 82
 83      // check for pod
 84      if (req.uri.path.first == "pod")
 85        { onServicePod; return }
 86
 87      // check for sub mod
 88      sub := subMods[req.modRel.path.first ?: ""]
 89      if (sub != null)
 90      {
 91        req.mod = sub
 92        req.modBase = req.modBase + `$req.modRel.path.first/`
 93        sub.onService
 94        return
 95      }
 96
 97      // match req to Route
 98      match := router.match(req.modRel, req.method)
 99      if (match == null) throw DraftErr(404)
100      req.stash["draft.route.meta"] = match.meta
101
102      // access session here before response is commited so
103      // session cookie has a chance to be added to res header
104      dummy := flash
105
106      // allow pre-service
107      onBeforeService(match.args)
108      if (res.isDone) return
109
110      // delegate to Route.handler
111      h := match.route.handler
112      args := h.params.isEmpty ? null : [match.args]
113      weblet := h.parent == typeof ? this : h.parent.make
114      weblet.trap(h.name, args)
115
116      // allow post-service
117      onAfterService(match.args)
118
119      // store flash for next req
120      req.session["draft.flash"] = flash.res.ro.toImmutable
121
122      // TODO - force flush here?
123      // res.out.flush
124    }
125    catch (Err err)
126    {
127      if (err isnot DraftErr) err = DraftErr(500, err)
128      logErr(err)
129      onErr(err)
130    }
131    finally
132    {
133      // log requst
134      logMod?.onService
135    }
136  }
137
138  ** Service a pub request.
139  private Void onServicePub()
140  {
141    file := pubDir + req.uri[1..-1]
142    if (!file.exists) throw DraftErr(404)
143    FileWeblet(file).onService
144  }
145
146  ** Service a pod request.
147  private Void onServicePod()
148  {
149    // must have at least 3 path segments
150    path := req.uri.path
151    if (path.size < 2) throw DraftErr(404)
152
153    // lookup pod
154    pod := Pod.find(path[1], false)
155    if (pod == null) throw DraftErr(404)
156
157    // lookup file
158    file := pod.file(`/` + req.uri[2..-1], false)
159    if (file == null) throw DraftErr(404)
160    FileWeblet(file).onService
161  }
162
163//////////////////////////////////////////////////////////////////////////
164// Flash
165//////////////////////////////////////////////////////////////////////////
166
167  ** Get flash instance for this request.
168  Flash flash()
169  {
170    flash := req.stash["draft.flash"]
171    if (flash == null)
172    {
173      map := req.session["draft.flash"] ?: Str:Str[:]
174      req.stash["draft.flash"] = flash = Flash(map)
175    }
176    return flash
177  }
178
179//////////////////////////////////////////////////////////////////////////
180// Lifecycle
181//////////////////////////////////////////////////////////////////////////
182
183  ** Handle startup tasks.
184  override Void onStart()
185  {
186    if (logDir != null)
187    {
188      // start LogMod
189      logMod := LogMod { dir=logDir; filename="web-{YYYY-MM}.log"; fields=logFields }
190      this.logModRef.val = logMod
191      logMod.onStart
192    }
193  }
194
195  private LogMod? logMod() { logModRef.val }
196  private const AtomicRef logModRef := AtomicRef(null)
197
198//////////////////////////////////////////////////////////////////////////
199// Errs
200//////////////////////////////////////////////////////////////////////////
201
202  ** Handle an error condition during a request.
203  virtual Void onErr(DraftErr err)
204  {
205    // pick best err msg
206    msg := err.errCode == 500 && err.cause != null ? err.cause.msg : err.msg
207
208    // setup response if not already commited
209    if (!res.isCommitted)
210    {
211      res.statusCode = err.errCode
212      res.headers["Content-Type"] = "text/html; charset=UTF-8"
213      res.headers["Draft-Err-Msg"] = msg
214    }
215
216    // send HTML response
217    out := res.out
218    out.docType
219    out.html
220    out.head
221      .title.esc(err.msg).titleEnd
222      .style.w("pre,td { font-family:monospace; }
223                td:first-child { color:#888; padding-right:1em; }").styleEnd
224      .headEnd
225    out.body
226      // msg
227      out.h1.esc(err.msg).h1End
228      if (err.msg != msg) out.h2.esc(msg).h2End
229      if (!err.errCode.toStr.startsWith("4"))
230      {
231        out.hr
232        // req headers
233        out.table
234        req.headers.each |v,k| { out.tr.td.w(k).tdEnd.td.w(v).tdEnd.trEnd }
235        out.tableEnd
236        out.hr
237        // stack trace
238        out.pre.w(err.traceToStr).preEnd
239      }
240    out.bodyEnd
241    out.htmlEnd
242  }
243
244  ** Log error.
245  private Void logErr(DraftErr err)
246  {
247    // don't spam logs for favicon/robots.txt or 404
248    if (req.uri == `/favicon.ico`) return
249    if (req.uri == `/robots.txt`) return
250    if (err.errCode == 404) return
251
252    buf := StrBuf()
253    buf.add("$err.msg - $req.uri\n")
254    req.headers.each |v,k| { buf.add("  $k: $v\n") }
255    err.traceToStr.splitLines.each |s| { buf.add("  $s\n") }
256    log.err(buf.toStr.trim)
257  }
258
259  ** Log for DraftMod.
260  private const static Log log := Log.get("draft")
261}