//
// Copyright (c) 2011, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 17 May 10 Brian Frank Creation
//
using web
**
** ObixMod is an abstract base class that implements the
** standard plumbing for adding oBIX server side support.
** Standardized URIs handled by the base class:
**
** {modBase}/xsl debug style sheet
** {modBase}/about about object
** {modBase}/batch batch operation
** {modBase}/watchService watch service
** {modBase}/watch/{id} watch
**
** All other URIs to the mod are automatically handled
** by the following callbacks:
** - GET: `onRead`
** - PUT: `onWrite`
** - POST: `onInvoke`
**
const abstract class ObixMod : WebMod
{
//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////
**
** Construct with the given map for 'obix:About' parameters:
** - serverName: defaults to 'Env.cur.host'
** - vendorName: defaults to "Fantom"
** - vendorUrl: defaults to "https://fantom.org/"
** - productName: defaults to "Fantom"
** - productVersion: defaults to version of obix pod
** - productUrl: defaults to "https://fantom.org/"
**
new make(Str:Obj about := Str:Obj[:])
{
this.aboutServerName = about["serverName"] ?: Env.cur.host
this.aboutVendorName = about["vendorName"] ?: "Fantom"
this.aboutVendorUrl = about["vendorUrl"] ?: `https://fantom.org`
this.aboutProductName = about["productName"] ?: "Fantom"
this.aboutProductVer = about["productVersion"] ?: ObixMod#.pod.version.toStr
this.aboutProductUrl = about["productUrl"] ?: `https://fantom.org`
}
//////////////////////////////////////////////////////////////////////////
// Service
//////////////////////////////////////////////////////////////////////////
override Void onService()
{
// handle special built-in URIs
uri := req.modRel
try
{
cmd := uri.path.getSafe(0)
switch (cmd)
{
case null: onLobby; return
case "about": onAbout; return
case "batch": onBatch; return
case "watchService": onWatchService; return
case "watch": onWatch; return
case "xsl": onXsl; return
}
}
catch (Err e)
{
result := ObixErr.toObj("Internal error: $e.toStr", e)
writeResObj(result)
return
}
ObixObj? result
try
{
// route to callback
switch (req.method)
{
case "GET": result = onRead(uri)
case "PUT": result = onWrite(uri, readReqObj)
case "POST": result = onInvoke(uri, readReqObj)
default: res.sendErr(501); return
}
}
catch (UnresolvedErr e)
{
result = ObixErr.toUnresolvedObj(uri)
}
catch (Err e)
{
result = ObixErr.toObj("Internal error: $e.toStr", e)
}
// return response
writeResObj(result)
}
//////////////////////////////////////////////////////////////////////////
// Predefined Objects/URIs
//////////////////////////////////////////////////////////////////////////
private Void onLobby()
{
if (req.method != "GET") { res.sendErr(501); return }
writeResObj(lobby)
}
private Void onAbout()
{
if (req.method != "GET") { res.sendErr(501); return }
writeResObj(about)
}
private Void onXsl()
{
file := ObixMod#.pod.file(`/res/xsl.xml`)
FileWeblet(file).onService
}
//////////////////////////////////////////////////////////////////////////
// Batch
//////////////////////////////////////////////////////////////////////////
private Void onBatch()
{
// must be invoke POST
if (req.method != "POST") { res.sendErr(501); return }
// read input which must be <list>
in := readReqObj
if (in.elemName != "list") { writeResErr("Expecting BatchIn to be <list>"); return }
// process each list input operation and add item to output list
out := ObixObj { elemName="list"; contract=Contract.batchOut }
in.each |opIn|
{
// process a single input operation
Uri? opUri := ``
ObixObj? opOut
try
{
// ensure we have a uri value
if (opIn.elemName != "uri") throw Err("Batch op must be <uri>")
opUri = opIn.val as Uri
if (opUri == null) throw Err("Batch op missing <uri> val")
// relative to mod
normUri := opUri
uriStr := normUri.toStr
baseStr := req.modBase.toStr
if (uriStr.startsWith(baseStr))
normUri = uriStr[baseStr.size..-1].toUri
switch (opIn.contract.toStr)
{
case "obix:Read": opOut = onRead(normUri)
case "obix:Write": opOut = onWrite(normUri, opIn.get("in"))
case "obix:Invoke": opOut = onInvoke(normUri, opIn.get("in"))
default: opOut = ObixErr.toObj("Unknown batch op type: $opIn.contract")
}
}
catch (UnresolvedErr e) opOut = ObixErr.toUnresolvedObj(opUri)
catch (Err e) opOut = ObixErr.toObj("Failed: $opIn", e)
// add this op output to the overall output list
opOut.href = opUri
out.add(opOut)
}
writeResObj(out)
}
//////////////////////////////////////////////////////////////////////////
// Watches
//////////////////////////////////////////////////////////////////////////
private Void onWatchService()
{
// watchService/
uri := req.modRel
if (uri.path.size == 1)
{
if (req.method != "GET") { res.sendErr(501); return }
writeResObj(watchService)
return
}
// watchService/make
if (uri.path.size == 2 && uri.path[1] == "make")
{
if (req.method != "POST") { res.sendErr(501); return }
watch := watchOpen
writeResWatch(watch)
return
}
// anything else is unresolved error
writeResUnresolvedErr
}
private Void onWatch()
{
// all URIs must resolve to active watch
uri := req.modRel
watch := watch(uri.path.getSafe(1) ?: "?")
if (watch == null) { writeResUnresolvedErr; return }
// /watch/{id} returns watch itself
if (uri.path.size == 2) { writeResWatch(watch); return }
// handle /watch/{id}/{cmd}
cmd := uri.path.getSafe(2) ?: ""
if (cmd == "pollChanges") { onWatchPollChanges(watch); return }
if (cmd == "pollRefresh") { onWatchPollRefresh(watch); return }
if (cmd == "lease") { onWatchLease(watch); return }
if (cmd == "add") { onWatchAdd(watch); return }
if (cmd == "remove") { onWatchRemove(watch); return }
if (cmd == "delete") { onWatchDelete(watch); return }
// anything else is unresolved error
writeResUnresolvedErr
}
private Void onWatchLease(ObixModWatch watch)
{
// if write
if (req.method == "PUT")
{
val := readReqObj.val
if (val isnot Duration) throw Err("Expected lease val to be reltime, not $val")
watch.lease = val
}
// if read/write
if (req.method == "GET" || req.method == "PUT")
{
writeResObj(ObixObj { name="lease"; href=watchUri(watch)+`lease`; val = watch.lease })
return
}
res.sendErr(501)
}
private Void onWatchAdd(ObixModWatch watch)
{
if (req.method != "POST") { res.sendErr(501); return }
uris := readWatchIn
objs := watch.add(uris)
writeWatchOut(objs)
}
private Void onWatchRemove(ObixModWatch watch)
{
if (req.method != "POST") { res.sendErr(501); return }
uris := readWatchIn
watch.remove(uris)
writeResObj(ObixObj { val="Watch removed: $uris.size" })
}
private Void onWatchPollChanges(ObixModWatch watch)
{
if (req.method != "POST") { res.sendErr(501); return }
readReqObj // ignored
objs := watch.pollChanges
writeWatchOut(objs)
}
private Void onWatchPollRefresh(ObixModWatch watch)
{
if (req.method != "POST") { res.sendErr(501); return }
readReqObj // ignored
objs := watch.pollRefresh
writeWatchOut(objs)
}
private Void onWatchDelete(ObixModWatch watch)
{
if (req.method != "POST") { res.sendErr(501); return }
watch.delete
writeResObj(ObixObj { val="Watch deleted: $watch.id" })
}
private Uri[] readWatchIn()
{
// read input which must be <list>
obj := readReqObj
list := obj.get("hrefs")
if (list.elemName != "list") throw Err("Expecting WatchIn.hrefs to be <list>")
// process each list input operation and add item to output list
acc := Uri[,]
list.each |kid|
{
uri := kid.val as Uri
if (uri == null) throw Err("Expecting WatchIn child to be <uri>")
acc.add(uri)
}
return acc
}
private Void writeWatchOut(ObixObj[] objs)
{
list := ObixObj { elemName="list"; name="values" }
objs.each |obj|
{
if (obj.href == null) throw Err("Watched obj missing href: $obj")
list.add(obj)
}
writeResObj(ObixObj {
contract = Contract.watchOut
href = req.absUri + req.modBase
add(list) })
}
private Void writeResWatch(ObixModWatch watch)
{
obj := watch.toObixObj()
obj.href = watchUri(watch)
writeResObj(obj)
}
private Uri watchUri(ObixModWatch watch)
{
req.modBase + `watch/$watch.id/`
}
//////////////////////////////////////////////////////////////////////////
// Read/Write ObixObj
//////////////////////////////////////////////////////////////////////////
private ObixObj readReqObj()
{
str := req.in.readAllStr
return ObixObj.readXml(str.in)
}
private Void writeResObj(ObixObj obj)
{
buf := Buf()
out := buf.out
out.print("<?xml version='1.0' encoding='UTF-8'?>\n")
out.print("<?xml-stylesheet type='text/xsl' href='").print(req.modBase).print("xsl'?>\n")
obj.writeXml(out)
buf.flip
res.headers["Content-Type"] = "text/xml"
res.headers["Content-Length"] = buf.size.toStr
res.out.writeBuf(buf)
res.out.close
}
private Void writeResErr(Str msg, Err? cause := null)
{
writeResObj(ObixErr.toObj(msg, cause))
}
private Void writeResUnresolvedErr()
{
writeResObj(ObixErr.toUnresolvedObj(req.modRel))
}
//////////////////////////////////////////////////////////////////////////
// Requests
//////////////////////////////////////////////////////////////////////////
**
** Return the ObixObj representation of the given URI for
** the application. The URI is relative to the ObixMod
** base - see `web::WebReq.modRel`. Throw UnresolvedErr
** if URI doesn't map to a valid object. The resulting
** object must have its href set to the proper absolute
** URI according to 5.2 of the oBIX specification.
**
abstract ObixObj onRead(Uri uri)
**
** Write the value for the given URI and return the new
** representation. The URI is relative to the ObixMod
** base - see `web::WebReq.modRel`. Throw UnresolvedErr if URI
** doesn't map to a valid object. Throw ReadonlyErr if
** URI doesn't map to a writable object.
**
abstract ObixObj onWrite(Uri uri, ObixObj val)
**
** Invoke the operation for the given URI and return the result.
** The URI is relative to the ObixMod base - see `web::WebReq.modRel`
** Throw UnresolvedErr if URI doesn't map to a valid operation.
**
abstract ObixObj onInvoke(Uri uri, ObixObj arg)
//////////////////////////////////////////////////////////////////////////
// Overrides
//////////////////////////////////////////////////////////////////////////
**
** Get represenation of the Lobby object. Subclasses
** can override this to customize their lobby.
**
virtual ObixObj lobby()
{
ObixObj
{
href = req.absUri.plusSlash
contract = Contract.lobby
ObixObj { elemName = "ref"; name = "about"; href=`about/`; contract=Contract.about },
ObixObj { elemName = "op"; name = "batch"; href=`batch/`; in=Contract.batchIn; out=Contract.batchOut},
ObixObj { elemName = "ref"; name = "watchService"; href=`watchService/`; contract=Contract.watchService },
}
}
**
** Get represenation of the About object. Subclasses should
** override this to customize their about. See `make` to
** customize vendor and product fields.
**
virtual ObixObj about()
{
ObixObj
{
href = req.absUri.plusSlash
contract = Contract.about
ObixObj { name = "obixVersion"; val = "1.1" },
ObixObj { name = "serverName"; val = aboutServerName },
ObixObj { name = "serverTime"; val = DateTime.now },
ObixObj { name = "serverBootTime"; val = DateTime.boot },
ObixObj { name = "vendorName"; val = aboutVendorName },
ObixObj { name = "vendorUrl"; val = aboutVendorUrl },
ObixObj { name = "productName"; val = aboutProductName },
ObixObj { name = "productVersion"; val = aboutProductVer},
ObixObj { name = "productUrl"; val = aboutProductUrl },
ObixObj { name = "tz"; val = TimeZone.cur.fullName },
}
}
**
** Get represenation of the WatchService object. Subclasses
** can override this to customize their watch service.
**
virtual ObixObj watchService()
{
ObixObj
{
href = req.absUri.plusSlash
contract = Contract.watchService
ObixObj { elemName = "op"; name = "make"; href=`make/`; out=Contract.watch },
}
}
**
** Construct a new watch.
**
abstract ObixModWatch watchOpen()
**
** Find an existing watch by its identifier or return null.
**
abstract ObixModWatch? watch(Str id)
private const Str aboutServerName
private const Str aboutVendorName
private const Uri aboutVendorUrl
private const Str aboutProductName
private const Str aboutProductVer
private const Uri aboutProductUrl
}
**************************************************************************
** ObixModWatch
**************************************************************************
**
** ObixMod hooks for implementing server side watches. ObixMod manages
** the networking/protocol side of things, but subclasses are responsible
** for managing the actual URI subscription list and polling.
**
abstract class ObixModWatch
{
** Get unique idenifier for the watch. This string must be safe to
** use within a URI path (should not contain special chars or slashes)
abstract Str id()
** Get/set lease time
abstract Duration lease
** Add the given uris to watch and return current state. If
** there is an error for an individual uri, return an error object.
** Resulting objects must have hrefs which exactly match input uri.
abstract ObixObj[] add(Uri[] uris)
** Remove the given uris from the watch. Silently ignore bad uris.
abstract Void remove(Uri[] uris)
** Poll URIs which have changed since last poll.
** Resulting objects must have hrefs which exactly match input uri.
abstract ObixObj[] pollChanges()
** Poll all URIs in this watch.
** Resulting objects must have hrefs which exactly match input uri.
abstract ObixObj[] pollRefresh()
** Handle delete/cleanup of watch.
abstract Void delete()
** Map server side representation to its on-the-wire Obix representation.
virtual ObixObj toObixObj()
{
ObixObj {
it.elemName = "obj"
it.contract = Contract.watch
ObixObj { elemName="reltime"; name="lease"; href=`lease`; val = lease; writable=false },
ObixObj { elemName="op"; name="add"; href=`add`; in=Contract.watchIn; out=Contract.watchOut },
ObixObj { elemName="op"; name="remove"; href=`remove`; in=Contract.watchIn},
ObixObj { elemName="op"; name="pollChanges"; href=`pollChanges`; out=Contract.watchOut},
ObixObj { elemName="op"; name="pollRefresh"; href=`pollRefresh`; out=Contract.watchOut},
ObixObj { elemName="op"; name="delete"; href=`delete` },
}
}
** Debug string
override Str toStr() { "ObixModWatch $id" }
}