Skip to content

Commit

Permalink
Merge pull request #42 from huntc/more-versions
Browse files Browse the repository at this point in the history
WIP - DO NOT MERGE - Interpret a version from a ref
  • Loading branch information
huntc committed Sep 18, 2015
2 parents 7e6e93a + c80a76c commit 60cf876
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 104 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
jdk: oraclejdk8
language: scala
script: sbt test
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Project Documentation

[![Build Status](https://api.travis-ci.org/typesafehub/project-doc.png?branch=master)](https://travis-ci.org/typesafehub/project-doc)

A general purpose project documentation website.

## Setting up development environment
Expand Down
135 changes: 85 additions & 50 deletions app/controllers/Application.scala
Original file line number Diff line number Diff line change
@@ -1,103 +1,106 @@
package controllers

import java.io.File
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import javax.inject.{Named, Inject}

import akka.actor.ActorRef
import akka.pattern.{AskTimeoutException, ask}
import doc.{DocVersions, DocRenderer}
import doc.DocRenderer
import play.api.libs.MimeTypes
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.iteratee.{Enumerator, Iteratee}
import play.api.libs.iteratee.Iteratee
import play.api.libs.json.{JsError, JsSuccess, Json, JsPath}
import play.api.mvc._
import play.twirl.api.Html
import settings.Settings

import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future

object Application {

private[controllers] object MacBodyParser {
def apply(hmacHeader: String, secret: SecretKeySpec, algorithm: String) =
new MacBodyParser(hmacHeader, secret, algorithm)
def apply(hmacHeader: String, secret: SecretKeySpec, algorithm: String, maxBodySize: Int = 8192) =
new MacBodyParser(hmacHeader, secret, algorithm, maxBodySize)
}

private[controllers] class MacBodyParser(
hmacHeader: String,
secret: SecretKeySpec,
algorithm: String) extends BodyParser[Unit] {
algorithm: String,
maxBodySize: Int) extends BodyParser[Array[Byte]] {

def hex2bytes(hex: String): Array[Byte] =
hex.replaceAll("[^0-9A-Fa-f]", "").sliding(2, 2).toArray.map(Integer.parseInt(_, 16).toByte)

override def apply(request: RequestHeader): Iteratee[Array[Byte], Either[Result, Unit]] = {
override def apply(request: RequestHeader): Iteratee[Array[Byte], Either[Result, Array[Byte]]] = {
val hexSignature = request.headers.get(hmacHeader).map(_.dropWhile(_ != '=').drop(1)).getOrElse("")
val signature = hex2bytes(hexSignature)
Iteratee.fold[Array[Byte], Mac] {
Iteratee.fold[Array[Byte], (Mac, ArrayBuffer[Byte])] {
val mac = Mac.getInstance(algorithm)
mac.init(secret)
mac
} { (mac, bytes) =>
mac.update(bytes)
mac
(mac, ArrayBuffer.empty)
} {
case ((mac, buffer), bytes) =>
mac.update(bytes)
val newBuffer = if (buffer.length + bytes.length <= maxBodySize) buffer ++ bytes else buffer
(mac, newBuffer)
}.map {
case _ if signature.isEmpty => Left(Results.BadRequest(s"No $hmacHeader header present"))
case mac if mac.doFinal().sameElements(signature) => Right(())
case _ => Left(Results.Unauthorized("Bad signature"))
case _ if signature.isEmpty => Left(Results.BadRequest(s"No $hmacHeader header present"))
case (mac, buffer) if mac.doFinal().sameElements(signature) => Right(buffer.toArray)
case _ => Left(Results.Unauthorized("Bad signature"))
}
}
}

private def getDocRenderer(
host: String,
docRenderers: Map[String, ActorRef],
pathVersion: String => Option[String],
docRenderers: Map[String, Map[String, ActorRef]],
hostPrefixAliases: Map[String, String]): Option[ActorRef] = {

val hostPrefix = host.takeWhile(c => c != '.' && c != ':')
docRenderers.get(hostPrefix).orElse {
hostPrefixAliases.get(hostPrefix) match {
case Some(aliasedHostPrefix) => docRenderers.get(aliasedHostPrefix)
case None => None
}
}

val resolvedHostPrefix = if (docRenderers.contains(hostPrefix)) Some(hostPrefix) else hostPrefixAliases.get(hostPrefix)

for {
hp <- resolvedHostPrefix
dv <- docRenderers.get(hp)
pv <- pathVersion(hp)
dr <- dv.get(pv)
} yield dr
}
}

class Application @Inject() (
@Named("ConductRDocRenderer") conductrDocRenderer: ActorRef,
@Named("ConductRDocRenderer10") conductrDocRenderer10: ActorRef,
@Named("ConductRDocRenderer11") conductrDocRenderer11: ActorRef,
settings: Settings) extends Controller {

import Application._

private final val MacAlgorithm = "HmacSHA1"
private final val GitHubSignature = "X-Hub-Signature"

private val docRenderers = Map("conductr" -> conductrDocRenderer)

private val secret = new SecretKeySpec(settings.play.crypto.secret.getBytes, MacAlgorithm)

def renderIndex = Action {
Ok(views.html.conductr.index())
}

def renderDocsHome =
renderDocs("")
def renderDocsHome(version: String) =
renderDocs("", version)

def renderResources(path: String, version: String) =
renderDocs(path)
renderDocs(path, version)

def renderDocs(path: String, version: String = DocVersions.Latest) = Action.async { request =>
def renderDocs(path: String, version: String) = Action.async { request =>
request.headers.get(HOST) match {
case Some(host) =>
getDocRenderer(host, docRenderers, settings.application.hostAliases) match {
getDocRenderer(host, _ => Some(version), docRenderers, settings.application.hostAliases) match {
case Some(docRenderer) =>
docRenderer
.ask(DocRenderer.Render(path))(settings.doc.renderer.timeout)
.map {
case html: Html => Ok(html)
case resource: DocRenderer.Resource => renderResource(resource, path)
case DocRenderer.Redirect(rp) => Redirect(routes.Application.renderDocs(rp, DocVersions.Latest))
case DocRenderer.Redirect(rp, v) => Redirect(routes.Application.renderDocs(rp, v))
case DocRenderer.NotFound(rp) => NotFound(s"Cannot find $rp")
case DocRenderer.NotReady => ServiceUnavailable("Initializing documentation. Please try again in a minute.")
}
Expand All @@ -112,26 +115,58 @@ class Application @Inject() (
}
}

private def renderResource(resource: DocRenderer.Resource, path: String): Result = {
val fileName = path.drop(path.lastIndexOf('/') + 1)
Result(ResponseHeader(OK, Map[String, String](
CONTENT_LENGTH -> resource.size.toString,
CONTENT_TYPE -> MimeTypes.forFileName(fileName).getOrElse(BINARY)
)), resource.content)
}

def update() = Action(MacBodyParser(GitHubSignature, secret, MacAlgorithm)) { request =>
request.headers.get(HOST) match {
case Some(host) =>
getDocRenderer(host, docRenderers, settings.application.hostAliases) match {
case Some(docRenderer) =>
docRenderer ! DocRenderer.PropogateGetSite
Ok("Site update requested")
case None =>
NotFound(s"Unknown project: $host")
Json.parse(request.body).validate[String](webhookRef) match {
case JsSuccess(ref, _) =>
val branch = ref.reverse.takeWhile(_ != '/').reverse

def branchToVersion(hostPrefix: String): Option[String] =
branchesToVersions.get(hostPrefix).flatMap(_.get(branch))

getDocRenderer(host, branchToVersion, docRenderers, settings.application.hostAliases) match {
case Some(docRenderer) =>
docRenderer ! DocRenderer.PropogateGetSite
Ok("Site update requested")
case None =>
Ok(s"Site update requested for Unknown project: $host - ignoring")
}
case e: JsError =>
BadRequest(s"Cannot parse webhook: $e")
}
case None =>
NotFound("No host header")
}
}

private final val MacAlgorithm = "HmacSHA1"
private final val GitHubSignature = "X-Hub-Signature"

private val docRenderers = Map(
"conductr" -> Map(
"" -> conductrDocRenderer10,
"1.0.x" -> conductrDocRenderer10,
"1.1.x" -> conductrDocRenderer11
)
)

private val branchesToVersions = Map(
"conductr" -> Map(
"1.0" -> "1.0.x",
"master" -> "1.1.x"
)
)

private val secret = new SecretKeySpec(settings.play.crypto.secret.getBytes, MacAlgorithm)
private val webhookRef = (JsPath \ "ref").read[String]

private def renderResource(resource: DocRenderer.Resource, path: String): Result = {
val fileName = path.drop(path.lastIndexOf('/') + 1)
Result(ResponseHeader(OK, Map[String, String](
CONTENT_LENGTH -> resource.size.toString,
CONTENT_TYPE -> MimeTypes.forFileName(fileName).getOrElse(BINARY)
)), resource.content)
}

}
21 changes: 12 additions & 9 deletions app/doc/DocRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ object DocRenderer {
case class Render(path: String)

/**
* Redirect to a relative documentation path
* Redirect to a relative documentation path given a known version
*/
case class Redirect(path: String)
case class Redirect(path: String, version: String)

/**
* Path is not found
Expand Down Expand Up @@ -68,12 +68,12 @@ object DocRenderer {

def props(
docArchive: URI,
removeRootSegment: Boolean,
removeRootSegmentOfArchive: Boolean,
docRoot: Path,
docUri: String,
version: String,
wsClient: WSClient): Props =
Props(new DocRenderer(docArchive, removeRootSegment, docRoot, docUri, version, wsClient))
Props(new DocRenderer(docArchive, removeRootSegmentOfArchive, docRoot, docUri, version, wsClient))

private[doc] def unzip(input: Enumerator[Array[Byte]], removeRootSegment: Boolean)(implicit ec: ExecutionContext): Future[Path] = {
val archive = Files.createTempFile(null, null)
Expand Down Expand Up @@ -182,7 +182,7 @@ class DocRenderer(
implicit val cluster = Cluster(context.system)

override def preStart(): Unit = {
replicator ! Subscribe(SiteUpdateCounter, self)
replicator ! Subscribe(siteUpdateCounter, self)
self ! GetSite
}

Expand Down Expand Up @@ -217,19 +217,22 @@ class DocRenderer(

case PropogateGetSite =>
log.info(s"Notifying cluster of change for $docArchive")
replicator ! Update(SiteUpdateCounter, GCounter(), WriteLocal)(_ + 1)
replicator ! Update(siteUpdateCounter, GCounter(), WriteLocal)(_ + 1)

case Changed(SiteUpdateCounter, _: GCounter) =>
case Changed(siteUpdateCounter, _: GCounter) =>
self ! GetSite
}


private def siteUpdateCounter: String =
s"$SiteUpdateCounter/${self.path.name}/$version"

private def handleUnready: Receive = {
case _ => sender() ! NotReady
}

private def handleRendering(repo: FilesystemRepository, mdRenderer: PlayDoc, toc: Html, toolbar: Html, cache: Cache[Html]): Receive = {
case Render("") =>
sender() ! Redirect(IndexPath)
sender() ! Redirect(IndexPath, version)

case Render(path) if !path.contains(".") =>
cache(path) {
Expand Down
5 changes: 0 additions & 5 deletions app/doc/DocVersions.scala

This file was deleted.

36 changes: 27 additions & 9 deletions app/modules/ConductRDocRendererModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,53 @@ import java.nio.file.Paths
import javax.inject.{Provider, Inject, Singleton}

import akka.actor.{ActorRef, ActorSystem}
import doc.{DocVersions, DocRenderer}
import doc.DocRenderer
import play.api.{Configuration, Environment}
import play.api.inject.Module
import play.api.libs.ws.WSClient

object ConductRDocRendererModule {

@Singleton
class ConductRDocRendererProvider @Inject()(actorSystem: ActorSystem, wsClient: WSClient)
abstract class ConductRDocRendererProvider(actorSystem: ActorSystem, wsClient: WSClient, docArchive: URI, version: String)
extends Provider[ActorRef] {

private val renderer =
actorSystem.actorOf(DocRenderer.props(
new URI("https://github.com/typesafehub/conductr-doc/archive/master.zip"),
removeRootSegment = true,
docArchive,
removeRootSegmentOfArchive = true,
Paths.get("src/main/play-doc"),
controllers.routes.Application.renderDocsHome().url,
DocVersions.Latest,
wsClient), "conductr-doc-renderer")
controllers.routes.Application.renderDocsHome(version).url,
version,
wsClient), s"conductr-doc-renderer-$version")

override def get = renderer
}

@Singleton
class ConductRDocRendererProvider10 @Inject()(actorSystem: ActorSystem, wsClient: WSClient)
extends ConductRDocRendererProvider(
actorSystem,
wsClient,
new URI("https://github.com/typesafehub/conductr-doc/archive/1.0.zip"),
"1.0.x"
)

@Singleton
class ConductRDocRendererProvider11 @Inject()(actorSystem: ActorSystem, wsClient: WSClient)
extends ConductRDocRendererProvider(
actorSystem,
wsClient,
new URI("https://github.com/typesafehub/conductr-doc/archive/master.zip"),
"1.1.x"
)
}

class ConductRDocRendererModule extends Module {
import ConductRDocRendererModule._

def bindings(environment: Environment,
configuration: Configuration) = Seq(
bind[ActorRef].qualifiedWith("ConductRDocRenderer").toProvider[ConductRDocRendererProvider]
bind[ActorRef].qualifiedWith("ConductRDocRenderer10").toProvider[ConductRDocRendererProvider10],
bind[ActorRef].qualifiedWith("ConductRDocRenderer11").toProvider[ConductRDocRendererProvider11]
)
}
2 changes: 1 addition & 1 deletion app/views/mainNav.scala.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@(showIcon: Boolean)
<ul>
<li><a href="@routes.Application.renderDocsHome()">Documentation</a></li>
<li><a href="@routes.Application.renderDocsHome("")">Documentation</a></li>

@if(showIcon) {
<li><a href="http://www.typesafe.com">@svg.typesafeFullColor()</a></li>
Expand Down
8 changes: 4 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name := "project-doc"

version := "1.0-SNAPSHOT"

scalaVersion := "2.11.6"
scalaVersion := "2.11.7"

resolvers ++= Seq(
"spray repo" at "http://repo.spray.io",
Expand All @@ -20,12 +20,12 @@ libraryDependencies ++= Seq(
"org.webjars" % "foundation" % "5.5.1",
"org.webjars" % "prettify" % "4-Mar-2013",
"com.googlecode.kiama" %% "kiama" % "1.8.0",
"com.typesafe.conductr" %% "play24-conductr-bundle-lib" % "1.0.0",
"com.typesafe.conductr" %% "play24-conductr-bundle-lib" % "1.0.1",
"com.typesafe.play" %% "play-doc" % "1.2.3",
"io.spray" %% "spray-caching" % "1.3.3",
"com.typesafe.akka" %% "akka-testkit" % "2.3.12",
"org.scalatest" %% "scalatest" % "2.2.4" % "test",
"org.scalatestplus" %% "play" % "1.4.0-M3" % "test",
ws
"org.scalatestplus" %% "play" % "1.4.0-M3" % "test"
)

// Play
Expand Down
Loading

0 comments on commit 60cf876

Please sign in to comment.