diff options
author | Trygve Laugstøl <trygvis@inamo.no> | 2013-05-19 23:09:39 +0200 |
---|---|---|
committer | Trygve Laugstøl <trygvis@inamo.no> | 2013-05-19 23:09:39 +0200 |
commit | c7b619402f290fdf77312af5f426eb6bd0d5e5f7 (patch) | |
tree | c161afb788fbfb0610b6b70a7470b7d63a58cdc0 | |
parent | c569a4c58cf29778df28392b48e20ffebdbc518e (diff) | |
download | collection-json-explorer-scala-c7b619402f290fdf77312af5f426eb6bd0d5e5f7.tar.gz collection-json-explorer-scala-c7b619402f290fdf77312af5f426eb6bd0d5e5f7.tar.bz2 collection-json-explorer-scala-c7b619402f290fdf77312af5f426eb6bd0d5e5f7.tar.xz collection-json-explorer-scala-c7b619402f290fdf77312af5f426eb6bd0d5e5f7.zip |
o Implementing my own model of collection+json. My browser want to be more relaxed than what normally makes sense.
o Implementing the server side of the queries.
-rw-r--r-- | project/Build.scala | 2 | ||||
-rw-r--r-- | src/main/scala/io/trygvis/cj/Explorer.scala | 31 | ||||
-rw-r--r-- | src/main/scala/io/trygvis/cj/Json4sHelpers.scala | 47 | ||||
-rw-r--r-- | src/main/scala/io/trygvis/cj/Views.scala | 192 | ||||
-rw-r--r-- | src/main/scala/io/trygvis/cj/model.scala | 79 |
5 files changed, 229 insertions, 122 deletions
diff --git a/project/Build.scala b/project/Build.scala index 1542f8f..8f11f07 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -23,7 +23,7 @@ object Build extends sbt.Build { settings = buildSettings ++ Seq( description := "Collection+JSON Explorer", name := "collection-json-explorer", - libraryDependencies += "net.hamnaberg.rest" %% "scala-json-collection" % "2.1-SNAPSHOT", + libraryDependencies += "org.json4s" %% "json4s-native" % "3.1.0", libraryDependencies += "net.databinder" %% "unfiltered-filter" % "0.6.8", libraryDependencies += "net.databinder" %% "unfiltered-directives" % "0.6.8", libraryDependencies += "net.databinder" %% "unfiltered-jetty" % "0.6.8", diff --git a/src/main/scala/io/trygvis/cj/Explorer.scala b/src/main/scala/io/trygvis/cj/Explorer.scala index 9f8fd44..9c4206f 100644 --- a/src/main/scala/io/trygvis/cj/Explorer.scala +++ b/src/main/scala/io/trygvis/cj/Explorer.scala @@ -2,10 +2,9 @@ package io.trygvis.cj import scala.collection.JavaConversions._ import scala.io.Source -import java.net.{HttpURLConnection, URI} -import java.io.{Writer, StringWriter, PrintWriter, InputStreamReader} +import java.net.{URLEncoder, HttpURLConnection, URI} +import java.io._ import javax.servlet.http.HttpServletRequest -import net.hamnaberg.json.collection.{NativeJsonCollectionParser, JsonCollection} import unfiltered.request._ import unfiltered.response._ import unfiltered.filter._ @@ -53,13 +52,33 @@ class Browser extends Plan { views = viewsX(r) } yield { println("url=" + url) - val uri = URI.create(url) + + val targetParams = params flatMap { + case (key, value) if key.startsWith("param-") => + // It makes no sense to have duplicate query keys so just pick the first one. This is most likely the + // same as most web framework does when given multiple query parameters. + Some(key.substring(6), value.headOption getOrElse "") + case _ => + None + } + + println("targetParam=" + targetParams) + + val q = targetParams map { case (key, value) => + URLEncoder.encode(key, "utf-8") + "=" + URLEncoder.encode(value, "utf-8") + } + + println("q=" + q) + + val uri = URI.create(url + (if(q.nonEmpty) "?" + q.reduce (_ ++ "&" ++ _) else "")) + println("uri=" + uri) + val con = uri.toURL.openConnection().asInstanceOf[HttpURLConnection] con.setRequestProperty("accept", "application/vnd.collection+json") val content = Source.fromInputStream(con.getInputStream, "utf-8").mkString("") val headers = con.getHeaderFields.toMap filter {case (key, _) => key != null} - val result = NativeJsonCollectionParser.parseCollection(content) - Ok ~> Html5(views.data(uri, params, result, CjResponse(con.getResponseCode, con.getResponseMessage, headers))) + val result = Collection.parseCollection(new StringReader(content)) + Ok ~> Html5(views.data(uri, targetParams, result, CjResponse(con.getResponseCode, con.getResponseMessage, headers))) } } } diff --git a/src/main/scala/io/trygvis/cj/Json4sHelpers.scala b/src/main/scala/io/trygvis/cj/Json4sHelpers.scala new file mode 100644 index 0000000..0241e59 --- /dev/null +++ b/src/main/scala/io/trygvis/cj/Json4sHelpers.scala @@ -0,0 +1,47 @@ +package io.trygvis.cj + +import org.json4s._ + +object Json4sHelpers { + def getAsObjectList(obj: JObject, name: String): List[JObject] = { + (obj \ name) match { + case JArray(values) => values.collect{case o@JObject(_) => o} + case o@JObject(_) => List(o) + case _ => Nil + } + } + + def getAsValueList(obj: JObject, name: String): List[JValue] = { + (obj \ name) match { + case JNothing => Nil + case JArray(values) => values + case j => List(j) + } + } + + def getAsObject(obj: JObject, name: String): Option[JObject] = { + (obj \ name) match { + case o@JObject(_) => Some(o) + case _ => None + } + } + + def getAsString(obj: JObject, name: String): Option[String] = { + (obj \ name) match { + case JString(s) => Some(s) + case _ => None + } + } + + def replace(obj: JObject, name: String, value: JValue): JObject = { + val map = obj.obj.toMap - name + JObject(map.updated(name, value).toList) + } + + def filtered(obj: JObject): JObject = { + JObject(obj.obj.filter { + case JField(_, JNothing) => false + case _ => true + }) + } +} diff --git a/src/main/scala/io/trygvis/cj/Views.scala b/src/main/scala/io/trygvis/cj/Views.scala index 869677e..b4429d8 100644 --- a/src/main/scala/io/trygvis/cj/Views.scala +++ b/src/main/scala/io/trygvis/cj/Views.scala @@ -3,8 +3,6 @@ package io.trygvis.cj import scala.collection.JavaConversions._ import scala.xml.{Group, NodeSeq, Elem} import java.net.{MalformedURLException, URL, URI, URLEncoder} -import net.hamnaberg.json.collection.{Property, Link, Json4sHelpers, JsonCollection} -import net.hamnaberg.json.collection.Render.IMAGE import org.json4s.native.JsonMethods import java.io.{PrintWriter, StringWriter, Writer} @@ -41,18 +39,8 @@ class Views(baseUrl: String) { result.toString } - /* - mixin get_name(link, prefix, i) - - var name = typeof link.name == 'string' ? link.name : undefined - - var prompt = typeof link.prompt == 'string' ? link.prompt : undefined - - var prefix = typeof prefix == 'string' ? prefix + ': ' : '' - |#{prefix + (name || prompt || '#' + i)} - - */ def getName(link: Link, prefix: String, i: Int) = { -// var name = link.name - val name = Json4sHelpers.getAsString(link.underlying, "name") - prefix + name.orElse(link.prompt).getOrElse("#" + i) + prefix + link.name.orElse(link.prompt).getOrElse("#" + i) } def index = { @@ -132,17 +120,16 @@ class Views(baseUrl: String) { layout(content, None) } - def data(url: URI, params: Map[String, Seq[String]], result: Either[Throwable, JsonCollection], res: CjResponse) = { + def data(url: URI, targetParams: Map[String, String], result: Either[Throwable, Collection], res: CjResponse) = { def href(uri: URI) = { // val splits = uri.getPath.split('/') // for split in splits -// a(href=urlgenerator.render(split[1]), title='Explore #{split[1]}') #{split[0]} - uri.toURL.toExternalForm +// a(href=url generator.render(split[1]), title='Explore #{split[1]}') #{split[0]} + <a href={uri.toURL.toExternalForm}>{uri.toURL.toExternalForm}</a> } def link(link: Link) = { - val name = Json4sHelpers.getAsString(link.underlying, "name") <xml:group> <div> <a class="btn btn-primary btn-mini" href={render(link.href)}>Explore</a> @@ -154,53 +141,51 @@ class Views(baseUrl: String) { <dt>rel</dt> <dd>{tryLink(link.rel)}</dd> <dt>name</dt> - <dd>{name.getOrElse(notSet)}</dd> + <dd>{link.name.getOrElse(notSet)}</dd> <dt>prompt</dt> <dd>{link.prompt.getOrElse(notSet)}</dd> <dt>render</dt> - <dd>{link.render.map(_.name).getOrElse(notSet)}</dd> - {link.render match { - case Some(IMAGE) => - <dt>Image</dt> - <dd> - <a href={link.href.toURL.toExternalForm}> - <img src={link.href.toURL.toExternalForm} alt={name.getOrElse("")} title={name.getOrElse("")}/> - </a> - </dd> - case _ => - NodeSeq.Empty - }} + <dd>{link.render.getOrElse(notSet)}</dd> + {if(link.render == "image") + <dt>Image</dt> + <dd> + <a href={link.href.toURL.toExternalForm}> + <img src={link.href.toURL.toExternalForm} alt={link.name.getOrElse("")} title={link.name.getOrElse("")}/> + </a> + </dd> + } </dl> </xml:group> } - def meta(implicit cj: JsonCollection) = <xml:group> + def meta(implicit cj: Collection) = <xml:group> <div class="row-fluid"> <div class="span12"> <dl> <dt>version</dt> - <dd>{cj.version.name}</dd> + <dd>{cj.version}</dd> <dt>href</dt> - <dd> - <div>{href(cj.href)}</div> - </dd> + <dd>{cj.href.map(href).getOrElse(notSet)}</dd> </dl> </div> </div> - <div class="row-fluid"> - <div class="span12"> - <p> - <a class="btn btn-primary" href={render(cj.href)}>Explore</a> - <a class="btn btn-primary" href={cj.href.toURL.toExternalForm}>Raw</a> - <a class="btn btn-danger" href={delete(cj.href)}>Delete</a> - <form action='http://redbot.org'> - <input name='uri' value={url.toURL.toExternalForm} type='hidden'/> - <input name='req_hdr' value='Accept: application/vnd.collection+json' type='hidden'/> - <button class='btn btn-primary' type='submit'>Check with redbot.org</button> - </form> - </p> + {if(cj.href.isDefined) { + val url = cj.href.get + <div class="row-fluid"> + <div class="span12"> + <p> + <a class="btn btn-primary" href={render(url)}>Explore</a> + <a class="btn btn-primary" href={url.toURL.toExternalForm}>Raw</a> + <a class="btn btn-danger" href={delete(url)}>Delete</a> + <form action='http://redbot.org'> + <input name='uri' value={url.toURL.toExternalForm} type='hidden'/> + <input name='req_hdr' value='Accept: application/vnd.collection+json' type='hidden'/> + <button class='btn btn-primary' type='submit'>Check with redbot.org</button> + </form> + </p> + </div> </div> - </div> + }} {cj.links match { case Nil => NodeSeq.Empty @@ -208,22 +193,18 @@ class Views(baseUrl: String) { <xml:group> <h2>Collection Links</h2> {cj.links.zipWithIndex.map { case (l, i) => - val name = Json4sHelpers.getAsString(l.underlying, "name") - val title = l.prompt.orElse(name) match { - case Some(t) => ": " + t - case _ => "" - } - Group(Seq(<h3 id={"link-#" + (i + 1)}>{"Collection link #" + (i + 1) + title}</h3>, link(l))) + val title = l.prompt.orElse(l.name).map(": " + _).getOrElse("") + Group(Seq(<h3 id={"link-" + (i + 1)}>{"Collection link #" + (i + 1) + title}</h3>, link(l))) }} </xml:group> }} </xml:group> - // TODO: If the collection has prev/next links, add buttons to automaticaly navigate those. + // TODO: If the collection has prev/next links, add buttons to automatically navigate those. // TODO: Add ability to show the raw part of the collection. - def items(cj: JsonCollection) = { + def items(cj: Collection) = { - def itemLinks(cj: JsonCollection) = { + def itemLinks(cj: Collection) = { val first = cj.findLinkByRel("first") val prev = cj.findLinkByRel("prev") val next = cj.findLinkByRel("next") @@ -247,21 +228,24 @@ class Views(baseUrl: String) { val links = item.links <div class="item-container"> <h2 id={"item-" + (i + 1)}>Item #{i + 1}</h2> + {if(item.href.isDefined) { + val url = item.href.get <div class="fluid-row"> <div class="span12"> <p> - <a class="btn btn-primary btn-mini" href={render(item.href)}>Explore</a> - <a class="btn btn-primary btn-mini" href={item.href.toURL.toExternalForm}>Raw</a> + <a class="btn btn-primary btn-mini" href={render(url)}>Explore</a> + <a class="btn btn-primary btn-mini" href={url.toURL.toExternalForm}>Raw</a> <a class="btn btn-primary btn-mini" onClick='var item = $(this).parentsUntil("#items").last(); item.find(".item-form").toggle(); item.find(".item-data").toggle()'>Edit</a> - <a class="btn btn-danger btn-mini" href={delete(item.href)}>Delete</a> + <a class="btn btn-danger btn-mini" href={delete(url)}>Delete</a> </p> </div> </div> + }} <div class="fluid-row"> <div class="span12"> <dl> <dt>href</dt> - <dd><div>{href(item.href)}</div></dd> + <dd><div>{item.href.map(href).getOrElse(notSet)}</div></dd> </dl> </div> </div> @@ -277,25 +261,25 @@ class Views(baseUrl: String) { <div class="item-data fluid-row"> <div class="span12"> <table class="data-table"> - <!-- d.value is not the correct way to access the value --> - {item.data map { d => <tr><th>{d.name}</th><td>{d.value}</td></tr>}} + {item.data map { d => <tr><th>{d.name}</th><td>{d.value getOrElse ""}</td></tr>}} </table> </div> </div> + {if(item.href.isDefined) { + val uri = item.href.get + <xml:group> <h3 class="item-data" style="display: none">Data</h3> <div class="item-data fluid-row" style="display: none"> <div class="span12"> <form class="well" action="/write" method="POST"> - <input type="hidden" name="url" value={item.href.toURL.toExternalForm}/> + <input type="hidden" name="url" value={uri.toURL.toExternalForm}/> <table class="cj-form"> <tbody>{ item.data map { d => - val value = d.value.toString + val value = d.value getOrElse "" <tr> <th title={"name: " + d.name}> - <div> - <label for={d.name}>{d.prompt.getOrElse(d.name)}</label> - </div> + <label for={d.name}>{d.prompt.getOrElse(d.name)}</label> </th> <td> <input id={d.name} type="text" name={"param-" + d.name} value={value}/> @@ -318,6 +302,8 @@ class Views(baseUrl: String) { </form> </div> </div> + </xml:group> + }} </div> } } @@ -325,10 +311,10 @@ class Views(baseUrl: String) { </xml:group> } - def queries(implicit cj: JsonCollection) = { + def queries(implicit cj: Collection) = { {cj.queries.zipWithIndex map { case (query, i) => - val prompt = Json4sHelpers.getAsString(query.underlying, "prompt") - val name = Json4sHelpers.getAsString(query.underlying, "name") + val prompt = query.prompt + val name = query.name val title = prompt.orElse(name).getOrElse("Unnamed query #" + (i + 1)) <h2 id={"query-" + (i + 1)}>{title}</h2> @@ -337,8 +323,8 @@ class Views(baseUrl: String) { <form class="well" action="/render"> <input type="hidden" name="url" value={query.href.toURL.toExternalForm}/> <table class="cj-form"> - <tbody>{query.data map { d: Property => - val value = params(d.name).headOption.getOrElse(d.value.toString) + <tbody>{query.data map { d => + val value = targetParams.get(d.name).orElse(d.value) getOrElse "" <tr> <th title={"name: " + d.name}> <div> @@ -365,15 +351,19 @@ class Views(baseUrl: String) { }} } - def template(implicit cj: JsonCollection) = + def template(implicit cj: Collection) = <div class="row-fluid"> <div class="span12"> - <p>The data will be submitted to {href(cj.href)}</p> <form class="well" action="/write" method="POST"> - <input type="hidden" name="url" value={cj.href.toURL.toExternalForm}/> + {cj.href map {uri => + <xml:group> + <p>The data will be submitted to: {href(uri)}</p> + <input type="hidden" name="url" value={uri.toURL.toExternalForm}/> + </xml:group> + } getOrElse NodeSeq.Empty} <table class="cj-form"> <tbody>{cj.template.get.data map { d => - val value = params(d.name).headOption.getOrElse(d.value.toString) + val value = targetParams.get(d.name).orElse(d.value).getOrElse("") <tr> <th title={"name='" + d.name + "'"}> <div> @@ -389,11 +379,13 @@ class Views(baseUrl: String) { <tr> <th></th> <td> - <!-- - input.btn.btn-primary.disabled(type='submit', disabled) Write - p.help-block This collection has a template, but doesn't have a href which is required. - --> - <input class="btn btn-primary" type="submit" value="Write"/> + {cj.href match { + case Some(uri) => + <input class="btn btn-primary" type="submit" value="Write"/> + case None => + <input class="btn btn-primary disabled" type="submit" disabled="disabled" value="Write"/> + <p class="help-block">This collection has a template, but doesn't have a href which is required.</p> + }} </td> </tr> </tfoot> @@ -402,36 +394,7 @@ class Views(baseUrl: String) { </div> </div> -/* -block error - div(class='row-fluid') - dl - dt title - dd - if collection.error.title - | #{collection.error.title} - else - i Not set - dt code - dd - if collection.error.code - | #{collection.error.code} - else - i Not set - dt message - dd - if collection.error.message - - var lines = collection.error.message.split('\n') - if lines.length > 1 - for line in lines - | #{line.replace(/ /g, ' ')} - br - else - | #{collection.error.message} - else - i Not set -*/ - def error(implicit cj: JsonCollection) = { + def error(implicit cj: Collection) = { val e = cj.error.get val message = e.message map { m => val lines = m.split('\n') @@ -470,7 +433,7 @@ block error </div> } - def parsedContent(implicit cj: JsonCollection) = <xml:group> + def parsedContent(implicit cj: Collection) = <xml:group> <section id="meta"> <div class="page-header"> <h1>Meta</h1> @@ -606,8 +569,7 @@ block error </div> <script src="/javascripts/jquery-1.7.2.min.js"></script> - <script src="/javascripts/gui.js"></script> - <script src="/bootstrap-2.0.4/js/bootstrap.min.js"></script> + <script src="/bootstrap-2.0.4/js/bootstrap.js"></script> </body> </html> } diff --git a/src/main/scala/io/trygvis/cj/model.scala b/src/main/scala/io/trygvis/cj/model.scala new file mode 100644 index 0000000..5944470 --- /dev/null +++ b/src/main/scala/io/trygvis/cj/model.scala @@ -0,0 +1,79 @@ +package io.trygvis.cj + +import java.io.Reader +import org.json4s.native.JsonParser +import org.json4s._ +import scala.util.control.Exception._ + +import Json4sHelpers._ +import Collection._ +import java.net._ + +case class Link(underlying: JObject) { + val href: URI = getAsString(underlying, "href").flatMap(createUri).getOrElse(throw new MissingRequiredField("link", "href")) + val rel = getAsString(underlying, "rel").getOrElse(throw new MissingRequiredField("link", "href")) + val name = getAsString(underlying, "name") + val render = getAsString(underlying, "render") + val prompt = getAsString(underlying, "prompt") +} + +case class Data(underlying: JObject) { + val name = getAsString(underlying, "name").getOrElse(throw new MissingRequiredField("data", "name")) + val value = getAsString(underlying, "value") + val prompt = getAsString(underlying, "prompt") +} + +case class Item(underlying: JObject) { + val href = getAsString(underlying, "href").flatMap(createUri) + val links = getAsObjectList(underlying, "links").map(Link(_)) + val data = getAsObjectList(underlying, "data").map(Data(_)) +} + +case class Query(underlying: JObject) { + val href = getAsString(underlying, "href").flatMap(createUri).getOrElse(throw new MissingRequiredField("query", "href")) + val rel = getAsString(underlying, "rel").getOrElse(throw new MissingRequiredField("query", "rel")) + val name = getAsString(underlying, "name") + val prompt = getAsString(underlying, "prompt") + val data = getAsObjectList(underlying, "data").map(Data(_)) +} + +case class Template(underlying: JObject) { + val data = getAsObjectList(underlying, "data").map(Data(_)) +} + +case class Error(underlying: JObject) { + val title = getAsString(underlying, "title") + val code = getAsString(underlying, "code") + val message = getAsString(underlying, "message") +} + +case class Collection(underlying: JObject) { + val version = getAsString(underlying, "version").getOrElse("1.0") + val href = getAsString(underlying, "href").flatMap(createUri) + val links = getAsObjectList(underlying, "links") map { Link(_) } + val items = getAsObjectList(underlying, "items") map { Item(_) } + val queries = getAsObjectList(underlying, "queries") map { Query(_) } + val template = getAsObject(underlying, "template") map { Template(_) } + val error = getAsObject(underlying, "error") map { Error(_) } + + def findLinkByRel(rel: String) = links.find(_.rel == rel) +} + +object Collection { + def createUri(s: String): Option[URI] = allCatch.opt(URI.create(s)) + + def parseCollection(reader: Reader): Either[Throwable, Collection] = { + try { + val parsed = JsonParser.parse(reader, closeAutomatically = true) + parsed match { + case JObject(List(JField("collection", x@JObject(_)))) => { allCatch.either(Collection(x))} + case _ => throw new IllegalArgumentException("Unexpected json here. was\n %s".format(parsed)) + } + } + catch { + case e : Throwable => Left(e) + } + } +} + +class MissingRequiredField(val parent: String, val field: String) extends Exception |