Skip to content

Commit

Permalink
Hello GitHub!
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Birchall committed Sep 4, 2014
0 parents commit 6818616
Show file tree
Hide file tree
Showing 293 changed files with 33,912 additions and 0 deletions.
66 changes: 66 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.settings
.classpath
.project
*.iml
*.ipr
*.iws
dist/
lib_managed/
project/boot/
project/plugins/project/
project/target/
project/project/
target/

# use glob syntax.
syntax: glob
*.ser
*.class
*~
*.bak
#*.off
*.old

# eclipse conf file
.settings
.classpath
.project
.manager
.scala_dependencies

# idea
.idea
*.iml

# building
target
build
null
dist
test-output
build.log

# other scm
.svn
.CVS
.hg*

# switch to regexp syntax.
# syntax: regexp
# ^\.pc/

# Naughty output not in target directory
build.log
.DS_Store
derby.log
*.db
.lib
logs

# Vagrant
.vagrant
vagrant_ansible_inventory_default
tmp

# Development environment files
conf/application.dev.conf
9 changes: 9 additions & 0 deletions .sbtconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
java_major_version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | awk -F'.' '{ print $2 }')
if [ $java_major_version -ge 8 ]; then
PERM_OPT="-XX:MaxMetaspaceSize=386M"
else
PERM_OPT="-XX:MaxPermSize=256M"
fi
export SBT_OPTS="-XX:+CMSClassUnloadingEnabled ${PERM_OPT}"

3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Octoparts

[See documentation](http://m3dev.github.io/octoparts-site/)
99 changes: 99 additions & 0 deletions app/com/m3/octoparts/Global.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.m3.octoparts

import java.io.File
import java.util.concurrent.TimeUnit

import _root_.controllers.ControllersModule
import com.kenshoo.play.metrics.MetricsFilter
import com.m3.octoparts.cache.CacheModule
import com.m3.octoparts.http.HttpModule
import com.m3.octoparts.hystrix.{ HystrixMetricsLogger, HystrixModule }
import com.m3.octoparts.logging.PartRequestLogger
import com.m3.octoparts.repository.RepositoriesModule
import com.typesafe.config.ConfigFactory
import com.wordnik.swagger.config.{ ConfigFactory => SwaggerConfigFactory }
import com.wordnik.swagger.model.ApiInfo
import play.api._
import play.api.libs.concurrent.Akka
import play.api.mvc._
import scaldi.Module
import scaldi.play.ScaldiSupport

import scala.collection.concurrent.TrieMap
import scala.concurrent.duration._

object Global extends WithFilters(MetricsFilter) with ScaldiSupport {

val info = ApiInfo(
title = "Octoparts",
description = """Octoparts is an API aggregator service for your backend HTTP services.""",
termsOfServiceUrl = "<Choose your own terms of service url>",
contact = "<Put your own contact info here>",
license = "<Choose your own licence>",
licenseUrl = "<Choose your own licence URL>")

SwaggerConfigFactory.config.setApiInfo(info)

def applicationModule =
aggregator.module ::
new RepositoriesModule ::
new CacheModule ::
new HystrixModule ::
new HttpModule ::
new ControllersModule ::
new Module {
// Random stuff that doesn't belong in other modules
bind[PartRequestLogger] to PartRequestLogger
}

/**
* For each entry, V.getClass == K
*/
private val controllerCache = TrieMap[Class[_], Any]()

/**
* Caches controller instantiation which was shown to be expensive because of ScalDI.
*/
override def getControllerInstance[A](clazz: Class[A]): A = {
controllerCache.getOrElseUpdate(clazz, super.getControllerInstance(clazz)).asInstanceOf[A]
}

override def onStop(app: Application) = {
controllerCache.clear()
super.onStop(app)
}

// Load environment-specific application.${env}.conf, merged with the generic application.conf
override def onLoadConfig(config: Configuration, path: File, classloader: ClassLoader, mode: Mode.Mode): Configuration = {
val playEnv = config.getString("application.env").fold(mode.toString) { parsedEnv =>
// "test" mode should cause the environment to be "test" except when the parsedEnv is "ci",
// since CI/Jenkins needs its own test environment configuration
(mode.toString.toLowerCase, parsedEnv.toLowerCase) match {
case ("test", env) if env != "ci" => "test"
case (_, env) => env
}
}
Logger.debug(s"Play environment = $playEnv (mode = $mode, application.env = ${config.getString("application.env")}). Loading extra config from application.$playEnv.conf, if it exists.")
val modeSpecificConfig = config ++ Configuration(ConfigFactory.load(s"application.$playEnv.conf"))
super.onLoadConfig(modeSpecificConfig, path, classloader, mode)
}

override def onStart(app: Application) = {
super.onStart(app)

startPeriodicTasks(app)
}

/**
* Register any tasks that should be run on the global Akka scheduler.
* These tasks will automatically stop running when the app shuts down.
*/
def startPeriodicTasks(implicit app: Application): Unit = {
import play.api.libs.concurrent.Execution.Implicits.defaultContext

val hystrixLoggingInterval = app.configuration.underlying.getDuration("hystrix.logging.intervalMs", TimeUnit.MILLISECONDS).toInt.millis
Akka.system.scheduler.schedule(hystrixLoggingInterval, hystrixLoggingInterval) {
HystrixMetricsLogger.logHystrixMetrics()
}
}
}
18 changes: 18 additions & 0 deletions app/com/m3/octoparts/aggregator/PartRequestInfo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.m3.octoparts.aggregator

import com.m3.octoparts.model.{ PartRequest, RequestMeta }

/**
* All the information needed to request a given part
*
* @param requestMeta The common meta info that was supplied to the AggregateRequest
* @param partRequest Information about the individual part being requested
*/
case class PartRequestInfo(
requestMeta: RequestMeta,
partRequest: PartRequest,
noCache: Boolean = false) {

val partRequestId = partRequest.id.getOrElse(partRequest.partId)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.m3.octoparts.aggregator.handler

import com.m3.octoparts.http.HttpClientPool
import scaldi.Module

class AggregatorHandlersModule extends Module {

implicit val glueContext = play.api.libs.concurrent.Execution.Implicits.defaultContext

bind[HttpHandlerFactory] to new SimpleHttpHandlerFactory(inject[HttpClientPool])

}
25 changes: 25 additions & 0 deletions app/com/m3/octoparts/aggregator/handler/Handler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.m3.octoparts.aggregator.handler

import com.m3.octoparts.model.PartResponse
import com.m3.octoparts.model.config._

import scala.concurrent.Future

/**
* A Handler simply maps a Map[Param, String] to a Future[PartResponse]
* via it's #process method
*
* A Handler makes use of configuration data (partId, URI, registered params, etc.)
* and request-specific data (PartRequestInfo) to form a request to the external dependency.
* In most cases, this is done by parsing the PartRequestInfo
* into a form that can be consumed by a generic HystrixCommand (see [[HttpPartRequestHandler]])
*/
trait Handler {

type HandlerArguments = Map[ShortPartParam, String]

// Used primarily for creating a PartResponse, but also for logging purposes
def partId: String

def process(arguments: HandlerArguments): Future[PartResponse]
}
13 changes: 13 additions & 0 deletions app/com/m3/octoparts/aggregator/handler/HttpHandlerFactory.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.m3.octoparts.aggregator.handler

import com.m3.octoparts.model.config.HttpPartConfig

trait HttpHandlerFactory {

/**
* @param config a HttpCommandConfig entry
* @return a handler ready to be used
*/
def makeHandler(config: HttpPartConfig): Handler

}
142 changes: 142 additions & 0 deletions app/com/m3/octoparts/aggregator/handler/HttpPartRequestHandler.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.m3.octoparts.aggregator.handler

import java.net.{ URI, URLEncoder }

import com.m3.octoparts.http.{ HttpResponse, _ }
import com.m3.octoparts.hystrix._
import com.m3.octoparts.model.PartResponse
import com.m3.octoparts.model.config._
import com.netaporter.uri.Uri
import com.netaporter.uri.dsl._

import scala.concurrent.{ ExecutionContext, Future }
import scala.util.matching.Regex

/**
* Trait describing a handler for processing of generic HTTP PartRequestInfo requests
* that correspond to an external dependency
*/
trait HttpPartRequestHandler extends Handler {
handler =>

implicit def executionContext: ExecutionContext

def httpClient: HttpClientLike

def uriToInterpolate: String

def httpMethod: HttpMethod.Value

def additionalValidStatuses: Set[Int]

def hystrixExecutor: HystrixExecutor

/**
* A regex for matching "${...}" placeholders in strings
*/
private val PlaceholderReplacer: Regex = """\$\{([^\}]+)\}""".r

// def registeredParams: Set[PartParam]

/**
* Given arguments for this handler, builds a blocking HTTP request with the proper
* URI, header and body params and sends it asynchronously in the context of
* a Hystrix Command.
*
* Note that this Future should be used with care because it may
* contain a Failure instead of Success. Make sure to transform with
* .recover
*
* @param hArgs Preparsed HystrixArguments
* @return Future[PartResponse]
*/
def process(hArgs: HandlerArguments): Future[PartResponse] = {
hystrixExecutor.future {
createBlockingHttpRetrieve(hArgs).retrieve()
}.map {
createPartResponse
}
}

/**
* Returns a BlockingHttpRetrieve command
*
* @param hArgs Handler arguments
* @return a command object that will perform an HTTP request on demand
*/
def createBlockingHttpRetrieve(hArgs: HandlerArguments): BlockingHttpRetrieve = {
new BlockingHttpRetrieve {
val httpClient = handler.httpClient
val method = httpMethod
val uri = new URI(buildUri(hArgs))
val maybeBody = hArgs.collectFirst {
case (p, v) if p.paramType == ParamType.Body => v
}
val headers = collectHeaders(hArgs)
}
}

/**
* Transforms a HttpResponse case class into a PartResponse
* @param httpResp HttpResponse
* @return PartREsponse
*/
def createPartResponse(httpResp: HttpResponse) = PartResponse(
partId,
id = partId,
cookies = httpResp.cookies,
statusCode = Some(httpResp.status),
mimeType = httpResp.mimeType,
charset = httpResp.charset,
cacheControl = httpResp.cacheControl,
contents = httpResp.body,
errors = if (httpResp.status < 400 || additionalValidStatuses.contains(httpResp.status)) Nil else Seq(httpResp.message)
)

/**
* Turns a string into an escaped string for cookies
* @param c Cookie name or value
* @return escaped cookie string
*/
def escapeCookie(c: String) = URLEncoder.encode(c, "UTF-8")

/**
* Isolates the header-related arguments, taking care to escape Cookie headers
* @param hArgs arguments
* @return Map[String, String]
*/
def collectHeaders(hArgs: HandlerArguments): Seq[(String, String)] = {
hArgs.toSeq.collect {
case (p, v) if p.paramType == ParamType.Header => p.outputName -> v
case (p, v) if p.paramType == ParamType.Cookie => "Cookie" -> (escapeCookie(p.outputName) + "=" + escapeCookie(v))
}
}

/**
* Takes a "base" URI string in the format of "http://example.com/${hello}" and returns an interpolated
* string
*
* @param hArgs HttpArguments
* @return Uri
*/
private[handler] def buildUri(hArgs: HandlerArguments): Uri = {
val baseUri = interpolate(uriToInterpolate) { key =>
val maybeParamsVal: Option[String] = hArgs.collectFirst {
case (p, v) if p.paramType == ParamType.Path && p.outputName == key => v
}
maybeParamsVal.getOrElse("")
}
baseUri.addParams(hArgs.collect { case (p, v) if p.paramType == ParamType.Query => (p.outputName, v) }.toSeq)
}

/**
* Replace all instances of "${...}" placeholders in the given string
*
* @param stringToInterpolate the string that includes placeholders
* @param replacer a function that replaces the contents of the placeholder (excluding braces) with a string
* @return the interpolated string
*/
private def interpolate(stringToInterpolate: String)(replacer: String => String) =
PlaceholderReplacer.replaceAllIn(stringToInterpolate, { m => replacer(m.group(1)) })

}
Loading

0 comments on commit 6818616

Please sign in to comment.