From c569a4c58cf29778df28392b48e20ffebdbc518e Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Sun, 19 May 2013 01:14:42 +0200 Subject: o Initial import of the Scala version of the Collection+JSON explorer. --- src/main/scala/io/trygvis/cj/Explorer.scala | 73 ++++ src/main/scala/io/trygvis/cj/Views.scala | 613 ++++++++++++++++++++++++++++ 2 files changed, 686 insertions(+) create mode 100644 src/main/scala/io/trygvis/cj/Explorer.scala create mode 100644 src/main/scala/io/trygvis/cj/Views.scala (limited to 'src/main/scala/io') diff --git a/src/main/scala/io/trygvis/cj/Explorer.scala b/src/main/scala/io/trygvis/cj/Explorer.scala new file mode 100644 index 0000000..9f8fd44 --- /dev/null +++ b/src/main/scala/io/trygvis/cj/Explorer.scala @@ -0,0 +1,73 @@ +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 javax.servlet.http.HttpServletRequest +import net.hamnaberg.json.collection.{NativeJsonCollectionParser, JsonCollection} +import unfiltered.request._ +import unfiltered.response._ +import unfiltered.filter._ +import unfiltered.jetty._ + +case class CjResponse(code: Int, status: String, headers: java.util.Map[String, java.util.List[String]]) + +class Browser extends Plan { + import unfiltered.directives._, Directives._ + + def viewsX(req: HttpServletRequest) = { + + val (proto, port) = if (req.isSecure) + ("https", if (req.getServerPort == 443) "" else ":" + req.getServerPort) + else + ("http", if (req.getServerPort == 80) "" else ":" + req.getServerPort) + + val uri = proto + "://" + req.getServerName + port + "/" + new Views(uri) + } + + def queryParam(name: String) = Directive[Any, Any, String]({ request: HttpRequest[Any] => + request.parameterValues(name).headOption match { + case Some(value) => + Result.Success(value) + case None => + Result.Failure(BadRequest ~> ResponseString("Missing query parameter " + name + "\n")) + } + }) + + def intent = { + Path.Intent { + case "/" => + for { + _ <- GET + req <- underlying[HttpServletRequest] + views = viewsX(req) + } yield Ok ~> Html5(views.index) + case "/render" => + for { + _ <- GET + r <- underlying[HttpServletRequest] + url <- queryParam("url") + params <- QueryParams + views = viewsX(r) + } yield { + println("url=" + url) + val uri = URI.create(url) + 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))) + } + } + } +} + +object Explorer extends App { + Http(8080). + plan(new Browser). + resources(getClass.getResource("/public/")). + run() +} diff --git a/src/main/scala/io/trygvis/cj/Views.scala b/src/main/scala/io/trygvis/cj/Views.scala new file mode 100644 index 0000000..869677e --- /dev/null +++ b/src/main/scala/io/trygvis/cj/Views.scala @@ -0,0 +1,613 @@ +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} + +class Views(baseUrl: String) { + + val notSet = Not set + + def tryLink(s: String) = try { + val url = new URL(s) + {s} + } catch { + case _: MalformedURLException => {s} + } + + def render(uri: URI) = { + val s = uri.toURL.toExternalForm + baseUrl + "render?url=" + URLEncoder.encode(s, "utf-8") + } + + def delete(uri: URI) = { + val s = uri.toURL.toExternalForm + baseUrl + "render?url=" + URLEncoder.encode(s, "utf-8") + "&action=delete" + } + + def examples: Array[Elem] = Array("minimal", "collection", "item", "queries", "template", "error") map { name => + val cj = baseUrl + "examples/from-spec/" + name + ".collection+json" + {name} + } + + def getStackTrace(aThrowable: Throwable) = { + val result: Writer = new StringWriter() + val printWriter: PrintWriter = new PrintWriter(result) + aThrowable.printStackTrace(printWriter) + 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) + } + + def index = { + def innerContent = +
+

❤ Collection+JSON ❤

+ + +
 
+
 
+ +
+
+ +
+
+
+
+
+

About

+

This is an interactive explorer for the Collection+JSON hypermedia. Give it an URL and it will render is as good as it can.

+

+ The purpose is twofold: it is a useful tool to inspect + collections while developing or debugging an application. At + the same time it's useful to show the power of + hypermedia + by showing how much a generic user agent can do by using + only the generic Collection+JSON specification and not + knowing anything about your application. +

+

See also

+

+ There's a growing C+J community that's discussing on the Google Group. +

+

+ Reading the (quite light) formal specification is + useful. It also has a tutorial and some examples. +

+
+
+

Using

+

+ Feel to use this service! However, note that it's running on a free + Heroku + instance so it might fail, be slow or otherwise useless.

+

+ If you want to run it against your own local servers you can either run it yourself, or use apps like + localtunnel.com to make your application publicly available. +

+

The Source Code

+

+ The source code is available + here. +

+
+
+

Examples

+

+ The Employee application is a set of resources with employees + and departments. The application was made specifically for this explorer: + explore now! +

+

The specification contains a few example collections too which + you can explore: +

+
    + {examples.map {example =>
  • {example}
  • }} +
+
+
+ ; + + def content =
{innerContent}
+ layout(content, None) + } + + def data(url: URI, params: Map[String, Seq[String]], result: Either[Throwable, JsonCollection], 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 + } + + def link(link: Link) = { + val name = Json4sHelpers.getAsString(link.underlying, "name") + +
+ Explore + Raw +
+
+
href
+
{href(link.href)}
+
rel
+
{tryLink(link.rel)}
+
name
+
{name.getOrElse(notSet)}
+
prompt
+
{link.prompt.getOrElse(notSet)}
+
render
+
{link.render.map(_.name).getOrElse(notSet)}
+ {link.render match { + case Some(IMAGE) => +
Image
+
+ + {name.getOrElse("")} + +
+ case _ => + NodeSeq.Empty + }} +
+
+ } + + def meta(implicit cj: JsonCollection) = +
+
+
+
version
+
{cj.version.name}
+
href
+
+
{href(cj.href)}
+
+
+
+
+
+
+

+ Explore + Raw + Delete +

+ + + +
+

+
+
+ {cj.links match { + case Nil => + NodeSeq.Empty + case _ => + +

Collection Links

+ {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(, link(l))) + }} +
+ }} +
+ + // TODO: If the collection has prev/next links, add buttons to automaticaly navigate those. + // TODO: Add ability to show the raw part of the collection. + def items(cj: JsonCollection) = { + + def itemLinks(cj: JsonCollection) = { + val first = cj.findLinkByRel("first") + val prev = cj.findLinkByRel("prev") + val next = cj.findLinkByRel("next") + val last = cj.findLinkByRel("last") + if(first.isDefined || prev.isDefined || next.isDefined || last.isDefined) { +
+
+ {if(first.isDefined) First} + {if(prev.isDefined) Previous} + {if(next.isDefined) Next} + {if(last.isDefined) Last} +
+
+ } + } + + + {itemLinks(cj)} + { + cj.items.zipWithIndex.map { case (item, i) => + val links = item.links +
+

Item #{i + 1}

+
+
+

+ Explore + Raw + Edit + Delete +

+
+
+
+
+
+
href
+
{href(item.href)}
+
+
+
+ {if(links.nonEmpty) { + +

Item Links

+ {links.zipWithIndex.map { case (l, i2) => + Group(Seq(

Item Link #{i2 + 1}

, link(l))) + }} +
+ }} +

Data

+
+
+ + + {item.data map { d => }} +
{d.name}{d.value}
+
+
+ + +
+ } + } + {itemLinks(cj)} +
+ } + + def queries(implicit cj: JsonCollection) = { + {cj.queries.zipWithIndex map { case (query, i) => + val prompt = Json4sHelpers.getAsString(query.underlying, "prompt") + val name = Json4sHelpers.getAsString(query.underlying, "name") + val title = prompt.orElse(name).getOrElse("Unnamed query #" + (i + 1)) + +

{title}

+
+
+
+ + + {query.data map { d: Property => + val value = params(d.name).headOption.getOrElse(d.value.toString) + + + + + }} + + + + + + +
+
+ +
+
+ +
+ +
+
+
+
+ }} + } + + def template(implicit cj: JsonCollection) = +
+
+

The data will be submitted to {href(cj.href)}

+
+ + + {cj.template.get.data map { d => + val value = params(d.name).headOption.getOrElse(d.value.toString) + + + + + }} + + + + + + +
+
+ +
+
+ +
+ + +
+
+
+
+ +/* +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) = { + val e = cj.error.get + val message = e.message map { m => + val lines = m.split('\n') + lines.map(s => scala.xml.Text(s): NodeSeq).reduce(_ ++
++ _) + } + +
+
+
title
+
{Option(e.title).filter(!_.isEmpty).getOrElse(notSet)}
+
code
+
{e.code.getOrElse(notSet)}
+
message
+
{message.getOrElse("")}
+
+
+ } + + def httpResponse = { +
+
+
Request URL
+
{url}
+
+ + + + + {res.headers map { case (header, values) => { values map { value => + + + + + }}}} +
{res.code} {res.status}
{header}{tryLink(value)}
+
+ } + + def parsedContent(implicit cj: JsonCollection) = +
+ + {meta} +
+ {if(cj.items.nonEmpty) { +
+ + {items(cj)} +
+ }} + {if(cj.queries.nonEmpty) { +
+ + {queries} +
+ }} + {if(cj.template.isDefined) { +
+ + {template} +
+ }} + {if(cj.error.isDefined) { +
+ + {error} +
+ }} +
+ + def innerContent = + {result match { + case Left(ex) => +
+ +
{getStackTrace(ex)}
+
+ case Right(cj) => + {try { parsedContent(cj) } catch { case ex: Exception =>
Unable to process model: {ex.getMessage}
}} +
+ +
+
+
{JsonMethods.pretty(JsonMethods.render(cj.underlying))}
+
+
+
+
+ }} +
+ + {httpResponse} +
+
+ + def sidebar = + + def content = +
+
{sidebar}
+
{innerContent}
+
+ + layout(content, None) + } + + def layout(content: Elem, headSnippet: Option[String]) = + + + Collection+JSON Explorer + + + + + + + + + + +
+ {content} +
+ + + + + + +} -- cgit v1.2.3