Skip to content

Commit f9138b3

Browse files
Dao NgocDao Ngoc
authored andcommitted
2 parents 42f0c6f + a92935c commit f9138b3

File tree

11 files changed

+138
-74
lines changed

11 files changed

+138
-74
lines changed

CHANGELOG

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
2.13 (planned):
1+
2.14:
2+
* #244 Easier way to get request content as string and JSON
3+
* #245 Rename "atJs" to "atJson"
4+
* #248 Collect all routes
5+
* #249 Improve inheritance rule of route annotations
6+
* #250 CORS allow-origin should not be set for domain not specified in xitrum.conf
7+
8+
2.13:
9+
* #239 Readd feature: One action can have multiple routes
10+
* #236 Remove Swagger related routes when it is disabled
11+
* #145 Split Knockout.js to a separate module
212
* #234 xitrum.js: Fix bug XITRUM_BASE_URL does not exist
313
* #237 xitrum.js: Add withBaseUrl
4-
* #239 Readd feature: One action can have multiple routes
514
* #242 Add atJs; atJs("key") returns the JSON form of at("key")
6-
* #236 Remove Swagger related routes when it is disabled
715
* #238 CSRF token can be set in header
8-
* #145 Split Knockout.js to a separate module
916

1017
2.12:
1118
* #230 Fix bug Routes with trailing '/' are not matched

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ organization := "tv.cntt"
22

33
name := "xitrum"
44

5-
version := "2.13-SNAPSHOT"
5+
version := "2.14-SNAPSHOT"
66

77
scalaVersion := "2.10.3"
88

dev/README.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,21 @@ Create dependency graph
6060
Demo:
6161
http://ngocdaothanh.github.io/xitrum/guide/deps.html
6262

63-
See https://github.com/jrudolph/sbt-dependency-graph.
63+
Generate target/dependencies-compile.dot:
64+
65+
::
66+
67+
sbt dependency-dot
6468

6569
Convert dependencies-compile.dot to deps.png:
6670

6771
::
6872

6973
dot -Tpng dependencies-compile.dot > deps.png
7074

75+
See:
76+
https://github.com/jrudolph/sbt-dependency-graph.
77+
7178
Publish to local
7279
----------------
7380

project/plugins.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// Run sbt/sbt eclipse to create Eclipse project file
1+
// Run sbt eclipse to create Eclipse project file
22
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
33

4-
// Run sbt/sbt gen-idea to create IntelliJ project file
4+
// Run sbt gen-idea to create IntelliJ project file
55
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.5.2")

src/main/scala/xitrum/annotation/ActionAnnotations.scala

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,37 @@ object ActionAnnotations {
3838
val typeOfCachePageHour = universe.typeOf[CachePageHour]
3939
val typeOfCachePageMinute = universe.typeOf[CachePageMinute]
4040
val typeOfCachePageSecond = universe.typeOf[CachePageSecond]
41+
42+
def fromUniverse(annotations: Seq[universe.Annotation]): ActionAnnotations = {
43+
var ret = ActionAnnotations()
44+
45+
annotations.foreach { a =>
46+
val tpe = a.tpe
47+
48+
if (tpe <:< typeOfRoute)
49+
ret = ret.copy(routes = ret.routes :+ a)
50+
51+
else if (tpe <:< typeOfRouteOrder)
52+
ret = ret.copy(routeOrder = Some(a))
53+
54+
else if (tpe <:< typeOfSockJsCookieNeeded)
55+
ret = ret.copy(sockJsCookieNeeded = Some(a))
56+
57+
else if (tpe <:< typeOfSockJsNoWebSocket)
58+
ret = ret.copy(sockJsNoWebSocket = Some(a))
59+
60+
else if (tpe <:< typeOfError)
61+
ret = ret.copy(error = Some(a))
62+
63+
else if (tpe <:< typeOfCache)
64+
ret = ret.copy(cache = Some(a))
65+
66+
else if (tpe <:< typeOfSwagger)
67+
ret = ret.copy(swaggers = ret.swaggers :+ a)
68+
}
69+
70+
ret
71+
}
4172
}
4273

4374
case class ActionAnnotations(
@@ -55,41 +86,42 @@ case class ActionAnnotations(
5586
) {
5687
import ActionAnnotations._
5788

58-
def overrideMe(other: ActionAnnotations) = ActionAnnotations(
59-
routes ++ other.routes,
60-
other.routeOrder orElse routeOrder,
61-
other.sockJsCookieNeeded orElse sockJsCookieNeeded,
62-
other.sockJsNoWebSocket orElse sockJsNoWebSocket,
63-
other.error orElse error,
64-
other.cache orElse cache,
65-
swaggers ++ other.swaggers
89+
/**
90+
* Only inherit sockJsCookieNeeded, sockJsNoWebSocket, cache, and swaggers.
91+
* Do not inherit routes, routeOrder, and error.
92+
* Current values if exist will override those in ancestor.
93+
*/
94+
def inherit(ancestor: ActionAnnotations) = ActionAnnotations(
95+
routes,
96+
routeOrder,
97+
sockJsCookieNeeded orElse ancestor.sockJsCookieNeeded,
98+
sockJsNoWebSocket orElse ancestor.sockJsNoWebSocket,
99+
error,
100+
cache orElse ancestor.cache,
101+
ancestor.swaggers ++ swaggers
66102
)
67103

68-
def overrideMe(annotations: Seq[universe.Annotation]): ActionAnnotations = {
104+
/**
105+
* Only inherit sockJsCookieNeeded, sockJsNoWebSocket, cache, and swaggers.
106+
* Do not inherit routes, routeOrder, and error.
107+
* Current values if exist will override those in ancestor.
108+
*/
109+
def inherit(annotations: Seq[universe.Annotation]): ActionAnnotations = {
69110
var ret = this
70111
annotations.foreach { a =>
71112
val tpe = a.tpe
72113

73-
if (tpe <:< typeOfRoute)
74-
ret = ret.copy(routes = ret.routes :+ a)
75-
76-
else if (tpe <:< typeOfRouteOrder)
77-
ret = ret.copy(routeOrder = Some(a))
78-
79-
else if (tpe <:< typeOfSockJsCookieNeeded)
114+
if (sockJsCookieNeeded.isEmpty && tpe <:< typeOfSockJsCookieNeeded)
80115
ret = ret.copy(sockJsCookieNeeded = Some(a))
81116

82-
else if (tpe <:< typeOfSockJsNoWebSocket)
117+
else if (sockJsNoWebSocket.isEmpty && tpe <:< typeOfSockJsNoWebSocket)
83118
ret = ret.copy(sockJsNoWebSocket = Some(a))
84119

85-
else if (tpe <:< typeOfError)
86-
ret = ret.copy(error = Some(a))
87-
88-
else if (tpe <:< typeOfCache)
120+
else if (cache.isEmpty && tpe <:< typeOfCache)
89121
ret = ret.copy(cache = Some(a))
90122

91123
else if (tpe <:< typeOfSwagger)
92-
ret = ret.copy(swaggers = ret.swaggers :+ a)
124+
ret = ret.copy(swaggers = a +: ret.swaggers)
93125
}
94126
ret
95127
}

src/main/scala/xitrum/handler/down/SetCORS.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ class SetCORS extends ChannelDownstreamHandler with Log {
4848
else
4949
HttpHeaders.setHeader(response, ACCESS_CONTROL_ALLOW_ORIGIN, requestOrigin)
5050
} else {
51-
if (corsAllowOrigins.contains(requestOrigin))
52-
HttpHeaders.setHeader(response, ACCESS_CONTROL_ALLOW_ORIGIN, requestOrigin)
53-
else
54-
HttpHeaders.setHeader(response, ACCESS_CONTROL_ALLOW_ORIGIN, corsAllowOrigins.mkString(", "))
51+
if (corsAllowOrigins.contains(requestOrigin)) HttpHeaders.setHeader(response, ACCESS_CONTROL_ALLOW_ORIGIN, requestOrigin)
5552
}
5653
}
5754

src/main/scala/xitrum/routing/ActionTreeBuilder.scala

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package xitrum.routing
22

3+
import java.lang.reflect.Modifier
34
import scala.collection.mutable.ArrayBuffer
45
import scala.reflect.runtime.universe
56

@@ -10,43 +11,46 @@ import xitrum.annotation.ActionAnnotations
1011
* Intended for use by RouteCollector.
1112
*
1213
* For each .class file, RouteCollector uses ASM's ClassReader to load it, then
13-
* calls "addBranches" to pass its class name and interface names. Action is a
14-
* trait. Traits are seen by ASM as interfaces. At this step, we build trees of
15-
* interface -> children.
14+
* calls "addBranches" to pass its class name, super class name, and interface names.
15+
* At this step, we build trees of parent -> children.
1616
*
1717
* Lastly, RouteCollector calls "getConcreteActionsAndAnnotations" to get
18-
* concrete (non-trait) action classes and their annotations.
18+
* concrete (non-trait, non-abstract) action classes and their annotations.
1919
*
2020
* This class is immutable because it will be serialized to routes.cache.
2121
*
22-
* @param interface2Children Map to save trees; names are class names
22+
* @param parent2Children Map to save trees; names are class names
2323
*/
24-
private case class ActionTreeBuilder(interface2Children: Map[String, Seq[String]] = Map()) {
24+
private case class ActionTreeBuilder(parent2Children: Map[String, Seq[String]] = Map()) {
2525
def addBranches(
26-
childInternalName: String,
27-
parentInternalNames: Array[String]
26+
childInternalName: String,
27+
superInternalName: String,
28+
interfaceInternalNames: Array[String]
2829
): ActionTreeBuilder = {
29-
// Class name: xitrum.Action
30-
// Internal name: xitrum/Action
31-
def internalName2ClassName(internalName: String) = internalName.replace('/', '.')
30+
if (superInternalName == null || interfaceInternalNames == null) return this
3231

33-
if (parentInternalNames == null) return this
32+
// Optimize: Ignore Java and Scala default classes; these can be thousands
33+
if (childInternalName.startsWith("java/") ||
34+
childInternalName.startsWith("javax/") ||
35+
childInternalName.startsWith("scala/") ||
36+
childInternalName.startsWith("sun/") ||
37+
childInternalName.startsWith("com/sun/")) return this
3438

35-
val childClassName = internalName2ClassName(childInternalName)
39+
val parentInternalNames = Seq(superInternalName) ++ interfaceInternalNames
40+
val parentClassNames = parentInternalNames.map(internalName2ClassName _)
3641

37-
var i2c = interface2Children
38-
parentInternalNames.foreach { parentInternalName =>
39-
val parentClassName = internalName2ClassName(parentInternalName)
40-
if (interface2Children.isDefinedAt(parentClassName)) {
41-
val children = interface2Children(parentClassName)
42+
val childClassName = internalName2ClassName(childInternalName)
43+
val p2c = parentClassNames.foldLeft(parent2Children) { case (acc, parentClassName) =>
44+
if (acc.isDefinedAt(parentClassName)) {
45+
val children = parent2Children(parentClassName)
4246
val newChildren = children :+ childClassName
43-
i2c += parentClassName -> newChildren
47+
acc + (parentClassName -> newChildren)
4448
} else {
45-
i2c += parentClassName -> Seq(childClassName)
49+
acc + (parentClassName -> Seq(childClassName))
4650
}
4751
}
4852

49-
ActionTreeBuilder(i2c)
53+
ActionTreeBuilder(p2c)
5054
}
5155

5256
/**
@@ -69,19 +73,22 @@ private case class ActionTreeBuilder(interface2Children: Map[String, Seq[String]
6973
case Some(aa) => aa
7074

7175
case None =>
72-
val parents = klass.getInterfaces
73-
val parentAnnotations = parents.foldLeft(ActionAnnotations()) { case (acc, parent) =>
74-
if (classOf[Action].isAssignableFrom(parent)) {
75-
val aa = getActionAccumulatedAnnotations(parent.asInstanceOf[Class[_ <: Action]])
76-
acc.overrideMe(aa)
76+
val parentClasses = Seq(klass.getSuperclass) ++ klass.getInterfaces
77+
val parentAnnotations = parentClasses.foldLeft(ActionAnnotations()) { case (acc, parentClass) =>
78+
// parentClass is null if klass is a trait/interface
79+
if (parentClass == null) {
80+
acc
81+
} else if (classOf[Action].isAssignableFrom(parentClass)) {
82+
val aa = getActionAccumulatedAnnotations(parentClass.asInstanceOf[Class[_ <: Action]])
83+
acc.inherit(aa)
7784
} else {
7885
acc
7986
}
8087
}
8188

82-
val annotations = runtimeMirror.classSymbol(klass).asClass.annotations
83-
val ret = parentAnnotations.overrideMe(annotations)
84-
89+
val universeAnnotations = runtimeMirror.classSymbol(klass).asClass.annotations
90+
val thisAnnotationsOnly = ActionAnnotations.fromUniverse(universeAnnotations)
91+
val ret = thisAnnotationsOnly.inherit(parentAnnotations)
8592
cache += klass -> ret
8693
ret
8794
}
@@ -92,15 +99,20 @@ private case class ActionTreeBuilder(interface2Children: Map[String, Seq[String]
9299

93100
//----------------------------------------------------------------------------
94101

102+
// Class name: xitrum.Action
103+
// Internal name: xitrum/Action
104+
private def internalName2ClassName(internalName: String) = internalName.replace('/', '.')
105+
95106
private def getConcreteActions: Set[Class[_ <: Action]] = {
96107
var concreteActions = Set[Class[_ <: Action]]()
97108

98109
def traverseActionTree(className: String) {
99-
if (interface2Children.isDefinedAt(className)) {
100-
val children = interface2Children(className)
110+
if (parent2Children.isDefinedAt(className)) {
111+
val children = parent2Children(className)
101112
children.foreach { className =>
102-
val klass = Class.forName(className).asInstanceOf[Class[_ <: Action]]
103-
if (!klass.isInterface) concreteActions += klass // Not interface => concrete
113+
val klass = Class.forName(className)
114+
val concrete = !klass.isInterface && !Modifier.isAbstract(klass.getModifiers)
115+
if (concrete) concreteActions += klass.asInstanceOf[Class[_ <: Action]]
104116
traverseActionTree(className)
105117
}
106118
}

src/main/scala/xitrum/routing/RouteCollector.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class RouteCollector extends Log {
4545
try {
4646
if (entry.relPath.endsWith(".class")) {
4747
val reader = new ClassReader(entry.bytes)
48-
acc.addBranches(reader.getClassName, reader.getInterfaces)
48+
acc.addBranches(reader.getClassName, reader.getSuperName, reader.getInterfaces)
4949
} else {
5050
acc
5151
}

src/main/scala/xitrum/scope/request/RequestEnv.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import scala.collection.mutable.{Map => MMap}
55
import xitrum.{Config, Action}
66
import xitrum.handler.HandlerEnv
77
import xitrum.routing.Route
8+
import xitrum.util.Json
89

910
object RequestEnv {
1011
def inspectParamsWithFilter(params: MMap[String, _ <: Seq[AnyRef]]): String = {
@@ -73,5 +74,14 @@ trait RequestEnv extends ParamAccess {
7374

7475
lazy val at = new At
7576

76-
def atJs(key: String) = at.toJson(key)
77+
def atJson(key: String) = at.toJson(key)
78+
79+
lazy val requestContentString = {
80+
val channelBuffer = request.getContent
81+
channelBuffer.toString(Config.xitrum.request.charset)
82+
}
83+
84+
/** Ex: val json = requestContentJson[Map[String, String]] */
85+
def requestContentJson[T](implicit m: Manifest[T]): T =
86+
Json.parse[T](requestContentString)(m)
7787
}

src/main/scala/xitrum/view/Responder.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,7 @@ trait Responder extends Js with Flash {
347347

348348
/**
349349
* To respond event source, call this method as many time as you want.
350-
* Event Source response is a special kind of chunked response.
351-
* Data must be Must be UTF-8.
350+
* Event Source response is a special kind of chunked response, data must be UTF-8.
352351
* See:
353352
* - http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.3.3.html#section-94
354353
* - http://dev.w3.org/html5/eventsource/

0 commit comments

Comments
 (0)