Skip to content

Commit

Permalink
Better Delta support to automatically handle HTML in applications whe…
Browse files Browse the repository at this point in the history
…n deltas are assigned

Added support to ProxyCache to create a Delta for processing script, img, and link tags to update to use caching
Fixes to Server.handle to avoid potential for exceptions not getting handled in certain circumstances
  • Loading branch information
darkfrog26 committed Jun 14, 2018
1 parent 6b084fc commit b9d49f2
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 70 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ node_modules/
npm-debug.log
*.js
*.js.map
package-lock.json
package-lock.json
temp/
22 changes: 13 additions & 9 deletions app/jvm/src/main/scala/io/youi/app/ServerApplication.scala
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ trait ServerApplication extends YouIApplication with Server {
* @return HttpHandler that has already been added to the server
*/
def deltas(function: HttpConnection => List[Delta]): HttpHandler = builder.handle { connection =>
val d = function(connection)
addDeltas(connection, d)
val d: List[Delta] = function(connection)
connection.deltas += d
}

def page(template: Content = ServerApplication.CanvasTemplate,
Expand All @@ -191,10 +191,15 @@ trait ServerApplication extends YouIApplication with Server {
}
}

def addDeltas(connection: HttpConnection, deltas: List[Delta]): Unit = {
if (deltas.nonEmpty) {
val current = connection.store.getOrElse[List[Delta]](ServerApplication.DeltaKey, Nil)
connection.store(ServerApplication.DeltaKey) = current ::: deltas
override protected def handleInternal(connection: HttpConnection): Unit = {
super.handleInternal(connection)

connection.response.content match {
case Some(content) if content.contentType == ContentType.`text/html` && connection.deltas.nonEmpty => {
connection.update(_.removeContent())
serveHTML(connection, content, Nil, includeApplication = false)
}
case _ => // Ignore
}
}

Expand All @@ -207,7 +212,7 @@ trait ServerApplication extends YouIApplication with Server {
val responseFields = responseMap(httpConnection).toList.map {
case (name, value) => s"""<input type="hidden" id="$name" value="$value"/>"""
}
val deltasList = httpConnection.store.getOrElse[List[Delta]](ServerApplication.DeltaKey, Nil) ::: deltas
val deltasList = httpConnection.deltas() ::: deltas
val applicationDeltas = if (includeApplication) {
val jsDeps = if (applicationJSDepsContent.nonEmpty) {
s"""<script src="$applicationJSDepsPath"></script>"""
Expand All @@ -233,6 +238,7 @@ trait ServerApplication extends YouIApplication with Server {
val d = applicationDeltas ::: deltasList
val selector = httpConnection.request.url.param("selector").map(Selector.parse)
val html = stream.stream(d, selector)
httpConnection.deltas.clear()
SenderHandler.handle(httpConnection, Content.string(html, ContentType.`text/html`), caching = CachingManager.NotCached)
}

Expand Down Expand Up @@ -343,6 +349,4 @@ object ServerApplication {
|</body>
|</html>
""".stripMargin.trim, ContentType.`text/html`)

val DeltaKey: String = "deltas"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import io.youi.http._
import io.youi.net._
import io.youi.server.handler.{CachingManager, ProxyCache}
import io.youi.server.dsl._
import io.youi.stream._

object ServerExampleApplication extends ExampleApplication with ServerApplication {
val generalPages: Page = page(GeneralPages)
Expand All @@ -24,15 +23,7 @@ object ServerExampleApplication extends ExampleApplication with ServerApplicatio
combined.any(
path.matches("/examples/.*[.]html"),
path.exact("/ui-examples.html")
) / Application / Delta.Process(
selector = ByTag("script"),
replace = true,
onlyOpenTag = true,
processor = (tag, content) => {
scribe.info(s"Process: $content")
content
}
) / ServerApplication.AppTemplate,
) / Application / ProxyCache.delta("/cache") / ServerApplication.AppTemplate,
path"/cookies.html" / CookiesExample,
path"/session.html" / SessionExample,
path"/cache" / ProxyCache(),
Expand Down
14 changes: 14 additions & 0 deletions server/src/main/scala/io/youi/http/HttpConnection.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.youi.http

import io.youi.server.Server
import io.youi.stream.Delta
import io.youi.{MapStore, Store}

class HttpConnection(val server: Server, val request: HttpRequest) {
Expand Down Expand Up @@ -28,6 +29,19 @@ class HttpConnection(val server: Server, val request: HttpRequest) {
}
}

object deltas {
private val key: String = "deltas"

def apply(): List[Delta] = store.getOrElse[List[Delta]](key, Nil)
def clear(): Unit = store.remove(key)

def +=(deltas: List[Delta]): Unit = store(key) = apply() ::: deltas
def +=(delta: Delta): Unit = this += List(delta)

def isEmpty: Boolean = apply().isEmpty
def nonEmpty: Boolean = apply().nonEmpty
}

def isFinished: Boolean = finished
def finish(): Unit = finished = true
}
32 changes: 17 additions & 15 deletions server/src/main/scala/io/youi/server/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,24 @@ trait Server extends HttpHandler with ErrorSupport {
SessionStore.dispose()
}

override def handle(connection: HttpConnection): Unit = {
try {
handleRecursive(connection, handlers())

// NotFound handling
if (connection.response.content.isEmpty && connection.response.status == HttpStatus.OK) {
connection.update { response =>
response.copy(status = HttpStatus.NotFound)
}
errorHandler.get.handle(connection, None)
}
} catch {
case t: Throwable => {
error(t)
errorHandler.get.handle(connection, Some(t))
override final def handle(connection: HttpConnection): Unit = try {
handleInternal(connection)
} catch {
case t: Throwable => {
error(t)
errorHandler.get.handle(connection, Some(t))
}
}

protected def handleInternal(connection: HttpConnection): Unit = {
handleRecursive(connection, handlers())

// NotFound handling
if (connection.response.content.isEmpty && connection.response.status == HttpStatus.OK) {
connection.update { response =>
response.copy(status = HttpStatus.NotFound)
}
errorHandler.get.handle(connection, None)
}
}

Expand Down
31 changes: 2 additions & 29 deletions server/src/main/scala/io/youi/server/dsl/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,41 +35,14 @@ package object dsl {

implicit class CachingManagerFilter(val caching: CachingManager) extends LastConnectionFilter(new ActionFilter(caching.handle))

implicit class DeltasFilter(val deltas: List[Delta]) extends ActionFilter(processDeltas(_, deltas))
implicit class DeltasFilter(val deltas: List[Delta]) extends ActionFilter(_.deltas += deltas)

implicit class DeltaFilter(delta: Delta) extends ActionFilter(processDeltas(_, List(delta)))
implicit class DeltaFilter(delta: Delta) extends ActionFilter(_.deltas += delta)

implicit class StringFilter(val s: String) extends ConnectionFilter {
override def filter(connection: HttpConnection): Option[HttpConnection] = PathPart.take(connection, s)
}

private[server] def processDeltas(connection: HttpConnection, deltas: List[Delta] = Nil): Unit = {
scribe.info(s"processing deltas: $deltas")
connection.response.content match {
case Some(content) => {
val stream: StreamableHTML = content match {
case c: FileContent => HTMLParser.cache(c.file)
case c: URLContent => HTMLParser.cache(c.url)
case c: StringContent => HTMLParser.cache(c.value)
}
val deltasList = connection.store.getOrElse[List[Delta]](DeltaKey, Nil) ::: deltas

if (deltasList.nonEmpty) {
val selector = connection.request.url.param("selector").map(Selector.parse)
val streamed = stream.stream(deltasList, selector)
connection.update { response =>
response.withContent(Content.string(streamed, content.contentType))
}
}
}
case None => { // No content
if (deltas.nonEmpty) {
scribe.warn(s"Not content set for processing of deltas: $deltas")
}
}
}
}

implicit class URLMatcherFilter(val matcher: URLMatcher) extends ConditionalFilter(c => matcher.matches(c.request.url))

case class ClassLoaderPath(directory: String = "", pathTransform: String => String = (s: String) => s) extends ConnectionFilter {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package io.youi.server.handler

import io.youi.http.{Content, HttpConnection, HttpStatus}
import io.youi.server.dsl

case class ContentHandler(content: Content, status: HttpStatus) extends HttpHandler {
override def handle(connection: HttpConnection): Unit = {
connection.update { response =>
response.copy(status = status, content = Some(content))
}
dsl.processDeltas(connection)
override def handle(connection: HttpConnection): Unit = connection.update { response =>
response.copy(status = status, content = Some(content))
}
}
23 changes: 23 additions & 0 deletions server/src/main/scala/io/youi/server/handler/ProxyCache.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.nio.file.{Path, Paths}

import io.youi.http.{Content, HttpConnection, HttpStatus}
import io.youi.net.ContentType
import io.youi.stream.{ByMultiple, ByTag, Delta}
import org.powerscala.io._

case class ProxyCache(directory: Path = Paths.get(System.getProperty("java.io.tmpdir"))) extends HttpHandler {
Expand All @@ -29,3 +30,25 @@ case class ProxyCache(directory: Path = Paths.get(System.getProperty("java.io.tm
case None => connection.update(_.withStatus(HttpStatus.BadRequest).withContent(Content.string("Must include `url` as GET parameter", ContentType.`text/plain`)))
}
}

object ProxyCache {
def delta(cachePath: String): Delta = Delta.Process(
selector = ByMultiple(ByTag("script"), ByTag("img"), ByTag("link")),
replace = true,
onlyOpenTag = true,
processor = (tag, content) => {
def fixContent(attributeName: String): String = {
val value = tag.attributes.getOrElse(attributeName, "")
if (value.toLowerCase.startsWith("http")) {
content.replaceAllLiterally(value, s"/cache?url=${URLEncoder.encode(value, "UTF-8")}")
} else {
content
}
}
tag.tagName.toLowerCase match {
case "script" | "img" => fixContent("src")
case "link" => fixContent("href")
}
}
)
}

0 comments on commit b9d49f2

Please sign in to comment.