/
RouteCollector.scala
340 lines (293 loc) · 14.4 KB
/
RouteCollector.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
package xitrum.routing
import java.io.File
import scala.collection.mutable.{ArrayBuffer, Map => MMap}
import scala.reflect.runtime.universe
import scala.util.control.NonFatal
import sclasner.{FileEntry, Scanner}
import xitrum.{Action, Config, Log, SockJsAction}
import xitrum.annotation._
import xitrum.sockjs.SockJsPrefix
case class DiscoveredAcc(
xitrumVersion: String,
normalRoutes: SerializableRouteCollection,
sockJsWithoutPrefixRoutes: SerializableRouteCollection,
sockJsMap: Map[String, SockJsClassAndOptions],
swaggerMap: Map[Class[_ <: Action], Swagger]
)
/** Scan all classes to collect routes from actions. */
object RouteCollector {
import ActionAnnotations._
def deserializeCacheFileOrRecollect(cachedFile: File, cl: ClassLoader): DiscoveredAcc = {
var acc = DiscoveredAcc(
"<Invalid Xitrum version>",
new SerializableRouteCollection,
new SerializableRouteCollection,
Map.empty[String, SockJsClassAndOptions],
Map.empty[Class[_ <: Action], Swagger]
)
// Only serialize/deserialize ActionTreeBuilder, which represents
// traits/classes extending Action, and their relationship. The relationship
// is for the (Swagger) annotation inheritance feature. The traits/classes
// are then loaded to get annotations.
val xitrumVersion = xitrum.version.toString
val actionTreeBuilder = Scanner.foldLeft(cachedFile, new ActionTreeBuilder(xitrumVersion), discovered(cl) _)
if (actionTreeBuilder.xitrumVersion != xitrumVersion) {
// The caller should see that the Xitrum version is invalid and act properly
acc
} else {
val ka = actionTreeBuilder.getConcreteActionsAndAnnotations(cl)
ka.foreach { case (klass, annotations) =>
acc = processAnnotations(acc, klass, annotations)
}
DiscoveredAcc(xitrumVersion, acc.normalRoutes, acc.sockJsWithoutPrefixRoutes, acc.sockJsMap, acc.swaggerMap)
}
}
//----------------------------------------------------------------------------
private def discovered(cl: ClassLoader)(acc: ActionTreeBuilder, entry: FileEntry): ActionTreeBuilder = {
// At ActionTreeBuilder, we can't use ASM or Javassist to get annotations
// (because they don't understand Scala annotations), we have to actually
// classes anyway, here we guess class name from .class file name and
// load the class.
if (!entry.relPath.endsWith(".class")) return acc
// Optimize: Ignore standard Java, standard Scala, Netty classes etc.; these can be thousands
val relPath = if (File.separatorChar != '/') entry.relPath.replace(File.separatorChar, '/') else entry.relPath
if (IgnoredPackages.isIgnored(relPath)) return acc
try {
val withoutExt = relPath.substring(0, relPath.length - ".class".length)
val className = withoutExt.replace('/', '.')
val klass = cl.loadClass(className)
val superclass = klass.getSuperclass.asInstanceOf[Class[_]]
val superclassNameo = if (superclass == null) None else Some(superclass.getName)
acc.addBranches(klass.getName, superclassNameo, klass.getInterfaces.map(_.getName))
} catch {
// Probably java.lang.NoClassDefFoundError: javax/servlet/http/HttpServlet
case e: java.lang.Error =>
acc
// Probably the .class file name -> class name guess was wrong
case NonFatal(e) =>
acc
}
}
private def processAnnotations(
acc: DiscoveredAcc,
klass: Class[_ <: Action],
annotations: ActionAnnotations
): DiscoveredAcc = {
val className = klass.getName
val fromSockJs = className.startsWith(classOf[SockJsPrefix].getPackage.getName)
val routes = if (fromSockJs) acc.sockJsWithoutPrefixRoutes else acc.normalRoutes
collectNormalRoutes(routes, className, annotations)
val newSockJsMap = collectSockJsMap(acc.sockJsMap, className, annotations)
collectErrorRoutes(routes, className, annotations)
val newSwaggerMap = collectSwagger(annotations) match {
case None => acc.swaggerMap
case Some(swagger) => acc.swaggerMap + (klass -> swagger)
}
DiscoveredAcc(acc.xitrumVersion, acc.normalRoutes, acc.sockJsWithoutPrefixRoutes, newSockJsMap, newSwaggerMap)
}
private def collectNormalRoutes(
routes: SerializableRouteCollection,
className: String,
annotations: ActionAnnotations
) {
var routeOrder = optRouteOrder(annotations.routeOrder) // -1: first, 1: last, 0: other
var cacheSecs = optCacheSecs(annotations.cache) // < 0: cache action, > 0: cache page, 0: no cache
var method_pattern_coll = ArrayBuffer.empty[(String, String)]
annotations.routes.foreach { a =>
listMethodAndPattern(a).foreach { m_p => method_pattern_coll.append(m_p) }
}
method_pattern_coll.foreach { case (method, pattern) =>
val compiledPattern = RouteCompiler.compile(pattern)
val serializableRoute = new SerializableRoute(method, compiledPattern, className, cacheSecs)
val coll = (routeOrder, method) match {
case (-1, "GET") => routes.firstGETs
case ( 1, "GET") => routes.lastGETs
case ( 0, "GET") => routes.otherGETs
case (-1, "POST") => routes.firstPOSTs
case ( 1, "POST") => routes.lastPOSTs
case ( 0, "POST") => routes.otherPOSTs
case (-1, "PUT") => routes.firstPUTs
case ( 1, "PUT") => routes.lastPUTs
case ( 0, "PUT") => routes.otherPUTs
case (-1, "PATCH") => routes.firstPATCHs
case ( 1, "PATCH") => routes.lastPATCHs
case ( 0, "PATCH") => routes.otherPATCHs
case (-1, "DELETE") => routes.firstDELETEs
case ( 1, "DELETE") => routes.lastDELETEs
case ( 0, "DELETE") => routes.otherDELETEs
case (-1, "WEBSOCKET") => routes.firstWEBSOCKETs
case ( 1, "WEBSOCKET") => routes.lastWEBSOCKETs
case ( 0, "WEBSOCKET") => routes.otherWEBSOCKETs
}
coll.append(serializableRoute)
}
}
private def collectErrorRoutes(
routes: SerializableRouteCollection,
className: String,
annotations: ActionAnnotations)
{
annotations.error.foreach { case a =>
val tpe = a.tree.tpe
val tpeString = tpe.toString
if (tpeString == TYPE_OF_ERROR_404.toString) routes.error404 = Some(className)
if (tpeString == TYPE_OF_ERROR_500.toString) routes.error500 = Some(className)
}
}
private def collectSockJsMap(
sockJsMap: Map[String, SockJsClassAndOptions],
className: String,
annotations: ActionAnnotations
): Map[String, SockJsClassAndOptions] =
{
var pathPrefix: String = null
annotations.routes.foreach { case a =>
val tpe = a.tree.tpe
val tpeString = tpe.toString
if (tpeString == TYPE_OF_SOCKJS.toString)
pathPrefix = a.tree.children.tail(0).productElement(0).asInstanceOf[universe.Constant].value.toString
}
if (pathPrefix == null) {
sockJsMap
} else {
val cl = Thread.currentThread.getContextClassLoader
val sockJsActorClass = cl.loadClass(className).asInstanceOf[Class[SockJsAction]]
val noWebSocket = annotations.sockJsNoWebSocket.isDefined
var cookieNeeded = Config.xitrum.response.sockJsCookieNeeded
if (annotations.sockJsCookieNeeded .isDefined) cookieNeeded = true
if (annotations.sockJsNoCookieNeeded.isDefined) cookieNeeded = false
sockJsMap + (pathPrefix -> new SockJsClassAndOptions(sockJsActorClass, !noWebSocket, cookieNeeded))
}
}
//----------------------------------------------------------------------------
/** -1: first, 1: last, 0: other */
private def optRouteOrder(annotationo: Option[universe.Annotation]): Int = {
annotationo match {
case None => 0
case Some(annotation) =>
val tpe = annotation.tree.tpe
val tpeString = tpe.toString
if (tpeString == TYPE_OF_FIRST.toString)
-1
else if (tpeString == TYPE_OF_LAST.toString)
1
else
0
}
}
/** < 0: cache action, > 0: cache page, 0: no cache */
private def optCacheSecs(annotationo: Option[universe.Annotation]): Int = {
annotationo match {
case None => 0
case Some(annotation) =>
val tpe = annotation.tree.tpe
val tpeString = tpe.toString
if (tpeString == TYPE_OF_CACHE_ACTION_DAY.toString)
-annotation.tree.children.tail(0).toString.toInt * 24 * 60 * 60
else if (tpeString == TYPE_OF_CACHE_ACTION_HOUR.toString)
-annotation.tree.children.tail(0).toString.toInt * 60 * 60
else if (tpeString == TYPE_OF_CACHE_ACTION_MINUTE.toString)
-annotation.tree.children.tail(0).toString.toInt * 60
else if (tpeString == TYPE_OF_CACHE_ACTION_SECOND.toString)
-annotation.tree.children.tail(0).toString.toInt
else if (tpeString == TYPE_OF_CACHE_PAGE_DAY.toString)
annotation.tree.children.tail(0).toString.toInt * 24 * 60 * 60
else if (tpeString == TYPE_OF_CACHE_PAGE_HOUR.toString)
annotation.tree.children.tail(0).toString.toInt * 60 * 60
else if (tpeString == TYPE_OF_CACHE_PAGE_MINUTE.toString)
annotation.tree.children.tail(0).toString.toInt * 60
else if (tpeString == TYPE_OF_CACHE_PAGE_SECOND.toString)
annotation.tree.children.tail(0).toString.toInt
else
0
}
}
/** @return Seq[(method, pattern)] */
private def listMethodAndPattern(annotation: universe.Annotation): Seq[(String, String)] = {
val tpe = annotation.tree.tpe
val tpeString = tpe.toString
if (tpeString == TYPE_OF_GET.toString)
getStrings(annotation).map(("GET", _))
else if (tpeString == TYPE_OF_POST.toString)
getStrings(annotation).map(("POST", _))
else if (tpeString == TYPE_OF_PUT.toString)
getStrings(annotation).map(("PUT", _))
else if (tpeString == TYPE_OF_PATCH.toString)
getStrings(annotation).map(("PATCH", _))
else if (tpeString == TYPE_OF_DELETE.toString)
getStrings(annotation).map(("DELETE", _))
else if (tpeString == TYPE_OF_WEBSOCKET.toString)
getStrings(annotation).map(("WEBSOCKET", _))
else
Seq()
}
private def getStrings(annotation: universe.Annotation): Seq[String] = {
annotation.tree.children.tail.map { tree => tree.productElement(0).asInstanceOf[universe.Constant].value.toString }
}
//----------------------------------------------------------------------------
private def collectSwagger(annotations: ActionAnnotations): Option[Swagger] = {
val universeAnnotations = annotations.swaggers
if (universeAnnotations.isEmpty) {
None
} else {
var swaggerArgs = Seq.empty[SwaggerArg]
universeAnnotations.foreach { annotation =>
annotation.tree.children.tail.foreach { scalaArg =>
// Ex:
// List(xitrum.annotation.Swagger.Response.apply, 200, "ID of the newly created article will be returned")
// List(xitrum.annotation.Swagger.StringForm.apply, "title", xitrum.annotation.Swagger.StringForm.apply$default$2)
// List(xitrum.annotation.Swagger.StringForm.apply, "title", "desc")
val children = scalaArg.children
val child0 = children(0).toString
if (child0 == "xitrum.annotation.Swagger.Resource.apply") {
val path = children(1).productElement(0).asInstanceOf[universe.Constant].value.toString
val desc =
if (children(2).toString.startsWith("xitrum.annotation.Swagger"))
""
else
children(2).productElement(0).asInstanceOf[universe.Constant].value.toString
swaggerArgs = swaggerArgs :+ Swagger.Resource(path, desc)
} else if (child0 == "xitrum.annotation.Swagger.Nickname.apply") {
val nickname = children(1).productElement(0).asInstanceOf[universe.Constant].value.toString
swaggerArgs = swaggerArgs :+ Swagger.Nickname(nickname)
} else if (child0 == "xitrum.annotation.Swagger.Produces.apply") {
val contentTypes = children.tail.map(_.productElement(0).asInstanceOf[universe.Constant].value.toString)
swaggerArgs = swaggerArgs :+ Swagger.Produces(contentTypes: _*)
} else if (child0 == "xitrum.annotation.Swagger.Consumes.apply") {
val contentTypes = children.tail.map(_.productElement(0).asInstanceOf[universe.Constant].value.toString)
swaggerArgs = swaggerArgs :+ Swagger.Consumes(contentTypes: _*)
} else if (child0 == "xitrum.annotation.Swagger.Summary.apply") {
val summary = children(1).productElement(0).asInstanceOf[universe.Constant].value.toString
swaggerArgs = swaggerArgs :+ Swagger.Summary(summary)
} else if (child0 == "xitrum.annotation.Swagger.Note.apply") {
val note = children(1).productElement(0).asInstanceOf[universe.Constant].value.toString
swaggerArgs = swaggerArgs :+ Swagger.Note(note)
} else if (child0 == "xitrum.annotation.Swagger.Response.apply") {
val code = children(1).toString.toInt
val desc = children(2).productElement(0).asInstanceOf[universe.Constant].value.toString
swaggerArgs = swaggerArgs :+ Swagger.Response(code, desc)
} else { // param or optional param
val name = children(1).productElement(0).asInstanceOf[universe.Constant].value.toString
val desc =
if (children(2).toString.startsWith("xitrum.annotation.Swagger"))
""
else
children(2).productElement(0).asInstanceOf[universe.Constant].value.toString
// Use reflection to create annotation
// Ex: xitrum.annotation.Swagger.StringForm.apply
val scalaClassName = child0.substring(0, child0.length - ".apply".length)
val builder = new StringBuilder(scalaClassName)
builder.setCharAt("xitrum.annotation.Swagger".length, '$')
// Ex: xitrum.annotation.Swagger$StringForm
val cl = Thread.currentThread.getContextClassLoader
val javaClassName = builder.toString
val klass = cl.loadClass(javaClassName)
val constructor = klass.getConstructor(classOf[String], classOf[String])
swaggerArgs = swaggerArgs :+ constructor.newInstance(name, desc).asInstanceOf[SwaggerArg]
}
}
}
Some(Swagger(swaggerArgs: _*))
}
}
}