Skip to content

Commit 60cf876

Browse files
committed
Merge pull request #42 from huntc/more-versions
WIP - DO NOT MERGE - Interpret a version from a ref
2 parents 7e6e93a + c80a76c commit 60cf876

File tree

12 files changed

+286
-104
lines changed

12 files changed

+286
-104
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
jdk: oraclejdk8
2+
language: scala
3+
script: sbt test

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Project Documentation
22

3+
[![Build Status](https://api.travis-ci.org/typesafehub/project-doc.png?branch=master)](https://travis-ci.org/typesafehub/project-doc)
4+
35
A general purpose project documentation website.
46

57
## Setting up development environment

app/controllers/Application.scala

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,106 @@
11
package controllers
22

3-
import java.io.File
43
import javax.crypto.Mac
54
import javax.crypto.spec.SecretKeySpec
65
import javax.inject.{Named, Inject}
76

87
import akka.actor.ActorRef
98
import akka.pattern.{AskTimeoutException, ask}
10-
import doc.{DocVersions, DocRenderer}
9+
import doc.DocRenderer
1110
import play.api.libs.MimeTypes
1211
import play.api.libs.concurrent.Execution.Implicits.defaultContext
13-
import play.api.libs.iteratee.{Enumerator, Iteratee}
12+
import play.api.libs.iteratee.Iteratee
13+
import play.api.libs.json.{JsError, JsSuccess, Json, JsPath}
1414
import play.api.mvc._
1515
import play.twirl.api.Html
1616
import settings.Settings
1717

18+
import scala.collection.mutable.ArrayBuffer
1819
import scala.concurrent.Future
1920

2021
object Application {
2122

2223
private[controllers] object MacBodyParser {
23-
def apply(hmacHeader: String, secret: SecretKeySpec, algorithm: String) =
24-
new MacBodyParser(hmacHeader, secret, algorithm)
24+
def apply(hmacHeader: String, secret: SecretKeySpec, algorithm: String, maxBodySize: Int = 8192) =
25+
new MacBodyParser(hmacHeader, secret, algorithm, maxBodySize)
2526
}
2627

2728
private[controllers] class MacBodyParser(
2829
hmacHeader: String,
2930
secret: SecretKeySpec,
30-
algorithm: String) extends BodyParser[Unit] {
31+
algorithm: String,
32+
maxBodySize: Int) extends BodyParser[Array[Byte]] {
3133

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

35-
override def apply(request: RequestHeader): Iteratee[Array[Byte], Either[Result, Unit]] = {
37+
override def apply(request: RequestHeader): Iteratee[Array[Byte], Either[Result, Array[Byte]]] = {
3638
val hexSignature = request.headers.get(hmacHeader).map(_.dropWhile(_ != '=').drop(1)).getOrElse("")
3739
val signature = hex2bytes(hexSignature)
38-
Iteratee.fold[Array[Byte], Mac] {
40+
Iteratee.fold[Array[Byte], (Mac, ArrayBuffer[Byte])] {
3941
val mac = Mac.getInstance(algorithm)
4042
mac.init(secret)
41-
mac
42-
} { (mac, bytes) =>
43-
mac.update(bytes)
44-
mac
43+
(mac, ArrayBuffer.empty)
44+
} {
45+
case ((mac, buffer), bytes) =>
46+
mac.update(bytes)
47+
val newBuffer = if (buffer.length + bytes.length <= maxBodySize) buffer ++ bytes else buffer
48+
(mac, newBuffer)
4549
}.map {
46-
case _ if signature.isEmpty => Left(Results.BadRequest(s"No $hmacHeader header present"))
47-
case mac if mac.doFinal().sameElements(signature) => Right(())
48-
case _ => Left(Results.Unauthorized("Bad signature"))
50+
case _ if signature.isEmpty => Left(Results.BadRequest(s"No $hmacHeader header present"))
51+
case (mac, buffer) if mac.doFinal().sameElements(signature) => Right(buffer.toArray)
52+
case _ => Left(Results.Unauthorized("Bad signature"))
4953
}
5054
}
5155
}
5256

5357
private def getDocRenderer(
5458
host: String,
55-
docRenderers: Map[String, ActorRef],
59+
pathVersion: String => Option[String],
60+
docRenderers: Map[String, Map[String, ActorRef]],
5661
hostPrefixAliases: Map[String, String]): Option[ActorRef] = {
62+
5763
val hostPrefix = host.takeWhile(c => c != '.' && c != ':')
58-
docRenderers.get(hostPrefix).orElse {
59-
hostPrefixAliases.get(hostPrefix) match {
60-
case Some(aliasedHostPrefix) => docRenderers.get(aliasedHostPrefix)
61-
case None => None
62-
}
63-
}
64+
65+
val resolvedHostPrefix = if (docRenderers.contains(hostPrefix)) Some(hostPrefix) else hostPrefixAliases.get(hostPrefix)
66+
67+
for {
68+
hp <- resolvedHostPrefix
69+
dv <- docRenderers.get(hp)
70+
pv <- pathVersion(hp)
71+
dr <- dv.get(pv)
72+
} yield dr
6473
}
6574
}
6675

6776
class Application @Inject() (
68-
@Named("ConductRDocRenderer") conductrDocRenderer: ActorRef,
77+
@Named("ConductRDocRenderer10") conductrDocRenderer10: ActorRef,
78+
@Named("ConductRDocRenderer11") conductrDocRenderer11: ActorRef,
6979
settings: Settings) extends Controller {
7080

7181
import Application._
7282

73-
private final val MacAlgorithm = "HmacSHA1"
74-
private final val GitHubSignature = "X-Hub-Signature"
75-
76-
private val docRenderers = Map("conductr" -> conductrDocRenderer)
77-
78-
private val secret = new SecretKeySpec(settings.play.crypto.secret.getBytes, MacAlgorithm)
79-
8083
def renderIndex = Action {
8184
Ok(views.html.conductr.index())
8285
}
8386

84-
def renderDocsHome =
85-
renderDocs("")
87+
def renderDocsHome(version: String) =
88+
renderDocs("", version)
8689

8790
def renderResources(path: String, version: String) =
88-
renderDocs(path)
91+
renderDocs(path, version)
8992

90-
def renderDocs(path: String, version: String = DocVersions.Latest) = Action.async { request =>
93+
def renderDocs(path: String, version: String) = Action.async { request =>
9194
request.headers.get(HOST) match {
9295
case Some(host) =>
93-
getDocRenderer(host, docRenderers, settings.application.hostAliases) match {
96+
getDocRenderer(host, _ => Some(version), docRenderers, settings.application.hostAliases) match {
9497
case Some(docRenderer) =>
9598
docRenderer
9699
.ask(DocRenderer.Render(path))(settings.doc.renderer.timeout)
97100
.map {
98101
case html: Html => Ok(html)
99102
case resource: DocRenderer.Resource => renderResource(resource, path)
100-
case DocRenderer.Redirect(rp) => Redirect(routes.Application.renderDocs(rp, DocVersions.Latest))
103+
case DocRenderer.Redirect(rp, v) => Redirect(routes.Application.renderDocs(rp, v))
101104
case DocRenderer.NotFound(rp) => NotFound(s"Cannot find $rp")
102105
case DocRenderer.NotReady => ServiceUnavailable("Initializing documentation. Please try again in a minute.")
103106
}
@@ -112,26 +115,58 @@ class Application @Inject() (
112115
}
113116
}
114117

115-
private def renderResource(resource: DocRenderer.Resource, path: String): Result = {
116-
val fileName = path.drop(path.lastIndexOf('/') + 1)
117-
Result(ResponseHeader(OK, Map[String, String](
118-
CONTENT_LENGTH -> resource.size.toString,
119-
CONTENT_TYPE -> MimeTypes.forFileName(fileName).getOrElse(BINARY)
120-
)), resource.content)
121-
}
122-
123118
def update() = Action(MacBodyParser(GitHubSignature, secret, MacAlgorithm)) { request =>
124119
request.headers.get(HOST) match {
125120
case Some(host) =>
126-
getDocRenderer(host, docRenderers, settings.application.hostAliases) match {
127-
case Some(docRenderer) =>
128-
docRenderer ! DocRenderer.PropogateGetSite
129-
Ok("Site update requested")
130-
case None =>
131-
NotFound(s"Unknown project: $host")
121+
Json.parse(request.body).validate[String](webhookRef) match {
122+
case JsSuccess(ref, _) =>
123+
val branch = ref.reverse.takeWhile(_ != '/').reverse
124+
125+
def branchToVersion(hostPrefix: String): Option[String] =
126+
branchesToVersions.get(hostPrefix).flatMap(_.get(branch))
127+
128+
getDocRenderer(host, branchToVersion, docRenderers, settings.application.hostAliases) match {
129+
case Some(docRenderer) =>
130+
docRenderer ! DocRenderer.PropogateGetSite
131+
Ok("Site update requested")
132+
case None =>
133+
Ok(s"Site update requested for Unknown project: $host - ignoring")
134+
}
135+
case e: JsError =>
136+
BadRequest(s"Cannot parse webhook: $e")
132137
}
133138
case None =>
134139
NotFound("No host header")
135140
}
136141
}
142+
143+
private final val MacAlgorithm = "HmacSHA1"
144+
private final val GitHubSignature = "X-Hub-Signature"
145+
146+
private val docRenderers = Map(
147+
"conductr" -> Map(
148+
"" -> conductrDocRenderer10,
149+
"1.0.x" -> conductrDocRenderer10,
150+
"1.1.x" -> conductrDocRenderer11
151+
)
152+
)
153+
154+
private val branchesToVersions = Map(
155+
"conductr" -> Map(
156+
"1.0" -> "1.0.x",
157+
"master" -> "1.1.x"
158+
)
159+
)
160+
161+
private val secret = new SecretKeySpec(settings.play.crypto.secret.getBytes, MacAlgorithm)
162+
private val webhookRef = (JsPath \ "ref").read[String]
163+
164+
private def renderResource(resource: DocRenderer.Resource, path: String): Result = {
165+
val fileName = path.drop(path.lastIndexOf('/') + 1)
166+
Result(ResponseHeader(OK, Map[String, String](
167+
CONTENT_LENGTH -> resource.size.toString,
168+
CONTENT_TYPE -> MimeTypes.forFileName(fileName).getOrElse(BINARY)
169+
)), resource.content)
170+
}
171+
137172
}

app/doc/DocRenderer.scala

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ object DocRenderer {
2828
case class Render(path: String)
2929

3030
/**
31-
* Redirect to a relative documentation path
31+
* Redirect to a relative documentation path given a known version
3232
*/
33-
case class Redirect(path: String)
33+
case class Redirect(path: String, version: String)
3434

3535
/**
3636
* Path is not found
@@ -68,12 +68,12 @@ object DocRenderer {
6868

6969
def props(
7070
docArchive: URI,
71-
removeRootSegment: Boolean,
71+
removeRootSegmentOfArchive: Boolean,
7272
docRoot: Path,
7373
docUri: String,
7474
version: String,
7575
wsClient: WSClient): Props =
76-
Props(new DocRenderer(docArchive, removeRootSegment, docRoot, docUri, version, wsClient))
76+
Props(new DocRenderer(docArchive, removeRootSegmentOfArchive, docRoot, docUri, version, wsClient))
7777

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

184184
override def preStart(): Unit = {
185-
replicator ! Subscribe(SiteUpdateCounter, self)
185+
replicator ! Subscribe(siteUpdateCounter, self)
186186
self ! GetSite
187187
}
188188

@@ -217,19 +217,22 @@ class DocRenderer(
217217

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

222-
case Changed(SiteUpdateCounter, _: GCounter) =>
222+
case Changed(siteUpdateCounter, _: GCounter) =>
223223
self ! GetSite
224224
}
225-
225+
226+
private def siteUpdateCounter: String =
227+
s"$SiteUpdateCounter/${self.path.name}/$version"
228+
226229
private def handleUnready: Receive = {
227230
case _ => sender() ! NotReady
228231
}
229232

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

234237
case Render(path) if !path.contains(".") =>
235238
cache(path) {

app/doc/DocVersions.scala

Lines changed: 0 additions & 5 deletions
This file was deleted.

app/modules/ConductRDocRendererModule.scala

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,53 @@ import java.nio.file.Paths
55
import javax.inject.{Provider, Inject, Singleton}
66

77
import akka.actor.{ActorRef, ActorSystem}
8-
import doc.{DocVersions, DocRenderer}
8+
import doc.DocRenderer
99
import play.api.{Configuration, Environment}
1010
import play.api.inject.Module
1111
import play.api.libs.ws.WSClient
1212

1313
object ConductRDocRendererModule {
1414

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

1918
private val renderer =
2019
actorSystem.actorOf(DocRenderer.props(
21-
new URI("https://github.com/typesafehub/conductr-doc/archive/master.zip"),
22-
removeRootSegment = true,
20+
docArchive,
21+
removeRootSegmentOfArchive = true,
2322
Paths.get("src/main/play-doc"),
24-
controllers.routes.Application.renderDocsHome().url,
25-
DocVersions.Latest,
26-
wsClient), "conductr-doc-renderer")
23+
controllers.routes.Application.renderDocsHome(version).url,
24+
version,
25+
wsClient), s"conductr-doc-renderer-$version")
2726

2827
override def get = renderer
2928
}
29+
30+
@Singleton
31+
class ConductRDocRendererProvider10 @Inject()(actorSystem: ActorSystem, wsClient: WSClient)
32+
extends ConductRDocRendererProvider(
33+
actorSystem,
34+
wsClient,
35+
new URI("https://github.com/typesafehub/conductr-doc/archive/1.0.zip"),
36+
"1.0.x"
37+
)
38+
39+
@Singleton
40+
class ConductRDocRendererProvider11 @Inject()(actorSystem: ActorSystem, wsClient: WSClient)
41+
extends ConductRDocRendererProvider(
42+
actorSystem,
43+
wsClient,
44+
new URI("https://github.com/typesafehub/conductr-doc/archive/master.zip"),
45+
"1.1.x"
46+
)
3047
}
3148

3249
class ConductRDocRendererModule extends Module {
3350
import ConductRDocRendererModule._
3451

3552
def bindings(environment: Environment,
3653
configuration: Configuration) = Seq(
37-
bind[ActorRef].qualifiedWith("ConductRDocRenderer").toProvider[ConductRDocRendererProvider]
54+
bind[ActorRef].qualifiedWith("ConductRDocRenderer10").toProvider[ConductRDocRendererProvider10],
55+
bind[ActorRef].qualifiedWith("ConductRDocRenderer11").toProvider[ConductRDocRendererProvider11]
3856
)
3957
}

app/views/mainNav.scala.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@(showIcon: Boolean)
22
<ul>
3-
<li><a href="@routes.Application.renderDocsHome()">Documentation</a></li>
3+
<li><a href="@routes.Application.renderDocsHome("")">Documentation</a></li>
44

55
@if(showIcon) {
66
<li><a href="http://www.typesafe.com">@svg.typesafeFullColor()</a></li>

build.sbt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ name := "project-doc"
44

55
version := "1.0-SNAPSHOT"
66

7-
scalaVersion := "2.11.6"
7+
scalaVersion := "2.11.7"
88

99
resolvers ++= Seq(
1010
"spray repo" at "http://repo.spray.io",
@@ -20,12 +20,12 @@ libraryDependencies ++= Seq(
2020
"org.webjars" % "foundation" % "5.5.1",
2121
"org.webjars" % "prettify" % "4-Mar-2013",
2222
"com.googlecode.kiama" %% "kiama" % "1.8.0",
23-
"com.typesafe.conductr" %% "play24-conductr-bundle-lib" % "1.0.0",
23+
"com.typesafe.conductr" %% "play24-conductr-bundle-lib" % "1.0.1",
2424
"com.typesafe.play" %% "play-doc" % "1.2.3",
2525
"io.spray" %% "spray-caching" % "1.3.3",
26+
"com.typesafe.akka" %% "akka-testkit" % "2.3.12",
2627
"org.scalatest" %% "scalatest" % "2.2.4" % "test",
27-
"org.scalatestplus" %% "play" % "1.4.0-M3" % "test",
28-
ws
28+
"org.scalatestplus" %% "play" % "1.4.0-M3" % "test"
2929
)
3030

3131
// Play

0 commit comments

Comments
 (0)