Skip to content

Commit af4ae1e

Browse files
committed
Generate package tree (with self nodes) on server side.
1 parent 5bf7aa3 commit af4ae1e

File tree

11 files changed

+197
-29
lines changed

11 files changed

+197
-29
lines changed

codepulse/src/main/scala/com/secdec/codepulse/data/bytecode/CodeForestBuilder.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import scala.collection.mutable.SortedSet
2525
import CodeForestBuilder._
2626
import java.lang.reflect.Modifier
2727
import com.secdec.codepulse.data.MethodSignatureParser
28-
import com.secdec.codepulse.data.trace.TreeNode
28+
import com.secdec.codepulse.data.trace.TreeNodeData
2929

3030
object CodeForestBuilder {
3131
val JSPGroupName = "JSPs"
@@ -44,7 +44,7 @@ class CodeForestBuilder(defaultTracedGroups: List[String]) {
4444
}
4545

4646
def result = {
47-
val lb = List.newBuilder[TreeNode]
47+
val lb = List.newBuilder[TreeNodeData]
4848
val tracedGroups = defaultTracedGroups.toSet
4949
for (root <- roots) root.visitTree { node =>
5050
val id = node.id
@@ -56,7 +56,7 @@ class CodeForestBuilder(defaultTracedGroups: List[String]) {
5656
case CodeTreeNodeKind.Grp | CodeTreeNodeKind.Pkg => Some(tracedGroups contains root.name)
5757
case _ => None
5858
}
59-
lb += TreeNode(id, parentId, name, kind, size, traced)
59+
lb += TreeNodeData(id, parentId, name, kind, size, traced)
6060
}
6161
lb.result()
6262
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.secdec.codepulse.data.trace
2+
3+
import com.fasterxml.jackson.core.{ JsonFactory, JsonGenerator }
4+
5+
import com.secdec.codepulse.data.bytecode.CodeTreeNodeKind
6+
7+
case class TreeNode(data: TreeNodeData, children: List[TreeNode])
8+
9+
case class PackageTreeNode(id: Option[Int], kind: CodeTreeNodeKind, label: String, methodCount: Int, traced: Option[Boolean], children: List[PackageTreeNode])
10+
11+
/** Builds/projects treemap and package tree data as JSON for client.
12+
* TODO: manage lifetime of cached data internally
13+
*
14+
* @author robertf
15+
*/
16+
class TreeBuilder(treeNodeData: TreeNodeDataAccess) {
17+
lazy val (roots, nodes) = {
18+
val roots = List.newBuilder[Int]
19+
val nodes = Map.newBuilder[Int, TreeNodeData]
20+
val children = collection.mutable.HashMap.empty[Int, collection.mutable.Builder[Int, List[Int]]]
21+
22+
def childrenFor(id: Int) = children.getOrElseUpdate(id, List.newBuilder[Int])
23+
24+
treeNodeData foreach { data =>
25+
nodes += data.id -> data
26+
(data.parentId match {
27+
case Some(parent) => childrenFor(parent)
28+
case None => roots
29+
}) += data.id
30+
}
31+
32+
val nodeMap = nodes.result
33+
34+
def buildNode(id: Int): TreeNode = {
35+
val children = childrenFor(id).result.map(buildNode)
36+
TreeNode(nodeMap(id), children)
37+
}
38+
39+
(roots.result.map(buildNode), nodeMap)
40+
}
41+
42+
lazy val packageTree = {
43+
// build up a package tree with the relevant data
44+
45+
/** A node is eligible for a self node if it is a package node that has at least one
46+
* package child and one non-package child (class/method).
47+
*/
48+
def isEligibleForSelfNode(node: TreeNode) = {
49+
node.data.kind == CodeTreeNodeKind.Pkg &&
50+
node.children.exists {
51+
case TreeNode(data, _) if data.kind == CodeTreeNodeKind.Pkg => true
52+
case _ => false
53+
} &&
54+
node.children.exists {
55+
case TreeNode(data, _) if data.kind == CodeTreeNodeKind.Cls || data.kind == CodeTreeNodeKind.Mth => true
56+
case _ => false
57+
}
58+
}
59+
60+
def countMethods(node: TreeNode): Int = {
61+
(node.children map {
62+
case TreeNode(data, _) if data.kind == CodeTreeNodeKind.Mth => 1
63+
case node => countMethods(node)
64+
}).sum
65+
}
66+
67+
def transform(node: TreeNode): PackageTreeNode = {
68+
// we don't want methods
69+
lazy val filteredChildren = node.children.filterNot { _.data.kind == CodeTreeNodeKind.Mth }
70+
71+
if (isEligibleForSelfNode(node)) {
72+
// split the node children depending on where they belong
73+
val (selfChildren, children) = filteredChildren.partition {
74+
case TreeNode(data, _) if data.kind == CodeTreeNodeKind.Cls || data.kind == CodeTreeNodeKind.Mth => true
75+
case _ => false
76+
}
77+
78+
// build the self node
79+
val selfNode = PackageTreeNode(Some(node.data.id), CodeTreeNodeKind.Pkg, "<self>", selfChildren.map(countMethods).sum, node.data.traced, selfChildren.map(transform))
80+
PackageTreeNode(None, node.data.kind, node.data.label, countMethods(node), node.data.traced, selfNode :: children.map(transform))
81+
} else {
82+
PackageTreeNode(Some(node.data.id), node.data.kind, node.data.label, countMethods(node), node.data.traced, filteredChildren.map(transform))
83+
}
84+
}
85+
86+
roots.map(transform)
87+
}
88+
}

codepulse/src/main/scala/com/secdec/codepulse/data/trace/TreeNodeData.scala

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,20 @@ import com.secdec.codepulse.data.bytecode.CodeTreeNodeKind
3131
* the size of a node is assumed to be the sum of its childrens' sizes.
3232
* @param traced whether or not this treenode is being traced; this value may be unspecified (None)
3333
*/
34-
case class TreeNode(id: Int, parentId: Option[Int], label: String, kind: CodeTreeNodeKind, size: Option[Int], traced: Option[Boolean])
34+
case class TreeNodeData(id: Int, parentId: Option[Int], label: String, kind: CodeTreeNodeKind, size: Option[Int], traced: Option[Boolean])
3535

3636
/** Access trait for tree node data.
3737
*
3838
* @author robertf
3939
*/
4040
trait TreeNodeDataAccess {
41-
def foreach(f: TreeNode => Unit): Unit
42-
def iterate[T](f: Iterator[TreeNode] => T): T
41+
def foreach(f: TreeNodeData => Unit): Unit
42+
def iterate[T](f: Iterator[TreeNodeData] => T): T
4343

44-
def getNode(id: Int): Option[TreeNode]
44+
def getNode(id: Int): Option[TreeNodeData]
4545

4646
def getNodeIdForSignature(signature: String): Option[Int]
47-
def getNodeForSignature(signature: String): Option[TreeNode]
47+
def getNodeForSignature(signature: String): Option[TreeNodeData]
4848

4949
def foreachMethodMapping(f: (String, Int) => Unit): Unit
5050
def iterateMethodMappings[T](f: Iterator[(String, Int)] => T): T
@@ -53,20 +53,20 @@ trait TreeNodeDataAccess {
5353
def mapMethodSignatures(signatures: Iterable[(String, Int)]): Unit = signatures foreach { case (signature, nodeId) => mapMethodSignature(signature, nodeId) }
5454

5555
def getNodeIdForJsp(jspClass: String): Option[Int]
56-
def getNodeForJsp(jspClass: String): Option[TreeNode]
56+
def getNodeForJsp(jspClass: String): Option[TreeNodeData]
5757

5858
def foreachJspMapping(f: (String, Int) => Unit): Unit
5959
def iterateJspMappings[T](f: Iterator[(String, Int)] => T): T
6060

6161
def mapJsp(jspClass: String, nodeId: Int): Unit
6262
def mapJsps(jsps: Iterable[(String, Int)]): Unit = jsps foreach { case (jspPath, nodeId) => mapJsp(jspPath, nodeId) }
6363

64-
def storeNode(node: TreeNode): Unit
65-
def storeNodes(nodes: Iterable[TreeNode]) = nodes foreach storeNode
64+
def storeNode(node: TreeNodeData): Unit
65+
def storeNodes(nodes: Iterable[TreeNodeData]) = nodes foreach storeNode
6666

6767
def updateTraced(id: Int, traced: Option[Boolean]): Unit
6868
def updateTraced(id: Int, traced: Boolean): Unit = updateTraced(id, Some(traced))
69-
def updateTraced(node: TreeNode, traced: Option[Boolean]): Unit = updateTraced(node.id, traced)
70-
def updateTraced(node: TreeNode, traced: Boolean): Unit = updateTraced(node.id, Some(traced))
69+
def updateTraced(node: TreeNodeData, traced: Option[Boolean]): Unit = updateTraced(node.id, traced)
70+
def updateTraced(node: TreeNodeData, traced: Boolean): Unit = updateTraced(node.id, Some(traced))
7171
def updateTraced(values: Iterable[(Int, Boolean)]): Unit = values foreach { case (id, traced) => updateTraced(id, Some(traced)) }
7272
}

codepulse/src/main/scala/com/secdec/codepulse/data/trace/slick/SlickTreeNodeDataAccess.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,28 @@
2020
package com.secdec.codepulse.data.trace.slick
2121

2222
import scala.slick.jdbc.JdbcBackend.Database
23-
import com.secdec.codepulse.data.trace.{ TreeNode, TreeNodeDataAccess }
23+
import com.secdec.codepulse.data.trace.{ TreeNodeData, TreeNodeDataAccess }
2424

2525
/** Slick-based TreeNodeDataAccess implementation.
2626
*
2727
* @author robertf
2828
*/
2929
private[slick] class SlickTreeNodeDataAccess(dao: TreeNodeDataDao, db: Database) extends TreeNodeDataAccess {
30-
def foreach(f: TreeNode => Unit) {
30+
def foreach(f: TreeNodeData => Unit) {
3131
iterate { _.foreach(f) }
3232
}
3333

34-
def iterate[T](f: Iterator[TreeNode] => T): T = {
34+
def iterate[T](f: Iterator[TreeNodeData] => T): T = {
3535
db withSession { implicit session =>
3636
dao.iterateWith(f)
3737
}
3838
}
3939

40-
def getNode(id: Int): Option[TreeNode] = db withSession { implicit session =>
40+
def getNode(id: Int): Option[TreeNodeData] = db withSession { implicit session =>
4141
dao get id
4242
}
4343

44-
def getNodeForSignature(sig: String): Option[TreeNode] = db withSession { implicit session =>
44+
def getNodeForSignature(sig: String): Option[TreeNodeData] = db withSession { implicit session =>
4545
dao getForSignature sig
4646
}
4747

@@ -67,7 +67,7 @@ private[slick] class SlickTreeNodeDataAccess(dao: TreeNodeDataDao, db: Database)
6767
dao storeMethodSignatures signatures
6868
}
6969

70-
def getNodeForJsp(jspPath: String): Option[TreeNode] = db withSession { implicit session =>
70+
def getNodeForJsp(jspPath: String): Option[TreeNodeData] = db withSession { implicit session =>
7171
dao getForJsp jspPath
7272
}
7373

@@ -93,11 +93,11 @@ private[slick] class SlickTreeNodeDataAccess(dao: TreeNodeDataDao, db: Database)
9393
dao storeJsps jsps
9494
}
9595

96-
def storeNode(node: TreeNode) = db withTransaction { implicit transaction =>
96+
def storeNode(node: TreeNodeData) = db withTransaction { implicit transaction =>
9797
dao storeNode node
9898
}
9999

100-
override def storeNodes(nodes: Iterable[TreeNode]) = db withTransaction { implicit transaction =>
100+
override def storeNodes(nodes: Iterable[TreeNodeData]) = db withTransaction { implicit transaction =>
101101
dao storeNodes nodes
102102
}
103103

codepulse/src/main/scala/com/secdec/codepulse/data/trace/slick/TreeNodeDataDao.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ package com.secdec.codepulse.data.trace.slick
2222
import scala.slick.driver.JdbcProfile
2323
import scala.slick.model.ForeignKeyAction
2424
import com.secdec.codepulse.data.bytecode.CodeTreeNodeKind
25-
import com.secdec.codepulse.data.trace._
25+
import com.secdec.codepulse.data.trace.{ TreeNodeData => TreeNode, _ }
2626

2727
/** The Slick DAO for tree node data.
2828
*

codepulse/src/main/scala/com/secdec/codepulse/tracer/AkkaTracingTarget.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import reactive.EventStream
3333
import com.secdec.codepulse.data.jsp.JspMapper
3434
import com.secdec.codepulse.data.trace.TraceId
3535
import com.secdec.codepulse.data.trace.TraceData
36+
import com.secdec.codepulse.data.trace.TreeBuilder
3637
import akka.actor.PoisonPill
3738

3839
//sealed trait TracingTargetEvent
@@ -76,6 +77,7 @@ trait TracingTarget {
7677

7778
def traceData: TraceData
7879
def transientData: TransientTraceData
80+
def treeBuilder: TreeBuilder
7981

8082
def requestDeletion(): Unit
8183
def notifyFinishedLoading(): Unit
@@ -100,7 +102,7 @@ object AkkaTracingTarget {
100102
// implicit Timeout used for the Akka ask (?) method in getState
101103
private implicit val timeout = new Timeout(5000)
102104

103-
private class TracingTargetImpl(val id: TraceId, actor: ActorRef, val traceData: TraceData, val transientData: TransientTraceData) extends TracingTarget with AskSupport {
105+
private class TracingTargetImpl(val id: TraceId, actor: ActorRef, val traceData: TraceData, val transientData: TransientTraceData, val treeBuilder: TreeBuilder) extends TracingTarget with AskSupport {
104106
def subscribeToStateChanges(sub: EventStream[TracingTargetState] => Unit) = actor ! Subscribe(sub)
105107
def requestNewTraceConnection() = actor ! RequestTraceConnect
106108
def requestTraceEnd() = actor ! RequestTraceEnd
@@ -112,10 +114,10 @@ object AkkaTracingTarget {
112114
def notifyFinishedLoading() = actor ! FinishedLoading
113115
}
114116

115-
def apply(actorSystem: ActorSystem, traceId: TraceId, traceData: TraceData, transientTraceData: TransientTraceData, jspMapper: Option[JspMapper]): TracingTarget = {
117+
def apply(actorSystem: ActorSystem, traceId: TraceId, traceData: TraceData, transientTraceData: TransientTraceData, treeBuilder: TreeBuilder, jspMapper: Option[JspMapper]): TracingTarget = {
116118
val props = Props { new AkkaTracingTarget(traceId, traceData, transientTraceData, jspMapper) }
117119
val actorRef = actorSystem.actorOf(props)
118-
new TracingTargetImpl(traceId, actorRef, traceData, transientTraceData)
120+
new TracingTargetImpl(traceId, actorRef, traceData, transientTraceData, treeBuilder)
119121
}
120122

121123
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Code Pulse: A real-time code coverage testing tool. For more information
3+
* see http://code-pulse.com
4+
*
5+
* Copyright (C) 2014 Applied Visions - http://securedecisions.avi.com
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package com.secdec.codepulse.tracer
21+
22+
import java.io.{ OutputStream, OutputStreamWriter }
23+
24+
import com.secdec.codepulse.data.trace._
25+
26+
import com.fasterxml.jackson.core.{ JsonFactory, JsonGenerator }
27+
import net.liftweb.http.OutputStreamResponse
28+
import net.liftweb.json.Printer
29+
30+
/** Streams package tree data using jackson.
31+
*
32+
* @author robertf
33+
*/
34+
object PackageTreeStreamer {
35+
private val Json = new JsonFactory
36+
37+
private def writeJson(jg: JsonGenerator)(node: PackageTreeNode) {
38+
jg.writeStartObject
39+
40+
for (id <- node.id) jg.writeNumberField("id", id)
41+
jg.writeStringField("kind", node.kind.label)
42+
jg.writeStringField("label", node.label)
43+
jg.writeNumberField("methodCount", node.methodCount)
44+
for (traced <- node.traced) jg.writeBooleanField("traced", traced)
45+
46+
if (!node.children.isEmpty) {
47+
jg writeArrayFieldStart "children"
48+
node.children.foreach(writeJson(jg))
49+
jg.writeEndArray
50+
}
51+
52+
jg.writeEndObject
53+
}
54+
55+
def streamPackageTree(packageData: List[PackageTreeNode]): OutputStreamResponse = {
56+
def writeData(out: OutputStream) {
57+
val jg = Json createGenerator out
58+
59+
try {
60+
jg.writeStartArray
61+
packageData.foreach(writeJson(jg))
62+
jg.writeEndArray
63+
} finally jg.close
64+
}
65+
66+
OutputStreamResponse(writeData, -1L, List("Content-Type" -> "application/json; charset=utf-8"), Nil, 200)
67+
}
68+
}

codepulse/src/main/scala/com/secdec/codepulse/tracer/TraceAPIServer.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ class TraceAPIServer(manager: TraceManager) extends RestHelper with Loggable {
169169
/** /trace-api/<target.id>/rename */
170170
val Rename = simpleTargetPath("rename")
171171

172+
/** /trace-api/<target.id>/packageTree */
173+
val PackageTree = simpleTargetPath("packageTree")
174+
172175
/** /trace-api/<target.id>/treemap */
173176
val Treemap = simpleTargetPath("treemap")
174177

@@ -286,6 +289,10 @@ class TraceAPIServer(manager: TraceManager) extends RestHelper with Loggable {
286289
} else OkResponse()
287290
}
288291

292+
// GET the trace's package tree as json
293+
case Paths.PackageTree(target) Get req =>
294+
PackageTreeStreamer.streamPackageTree(target.treeBuilder.packageTree)
295+
289296
// GET the trace's treemap data as json
290297
case Paths.Treemap(target) Get req =>
291298
TreemapDataStreamer.streamTreemapData(target.traceData)

codepulse/src/main/scala/com/secdec/codepulse/tracer/TraceManager.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import com.secdec.codepulse.data.jsp.JasperJspMapper
3535
import com.secdec.codepulse.data.trace.TraceId
3636
import com.secdec.codepulse.data.trace.TraceDataProvider
3737
import com.secdec.codepulse.data.trace.TraceData
38+
import com.secdec.codepulse.data.trace.TreeBuilder
3839

3940
object TraceManager {
4041
lazy val defaultActorSystem = {
@@ -87,7 +88,9 @@ class TraceManager(val actorSystem: ActorSystem) extends Observing {
8788
private def registerTrace(traceId: TraceId, traceData: TraceData, jspMapper: Option[JspMapper]) = {
8889
registerTraceId(traceId)
8990

90-
val target = AkkaTracingTarget(actorSystem, traceId, traceData, transientDataProvider get traceId, jspMapper)
91+
val treeBuilder = new TreeBuilder(traceData.treeNodeData)
92+
93+
val target = AkkaTracingTarget(actorSystem, traceId, traceData, transientDataProvider get traceId, treeBuilder, jspMapper)
9194
traces.put(traceId, target)
9295

9396
// cause a traceListUpdate when this trace's name changes

codepulse/src/main/scala/com/secdec/codepulse/tracer/TreemapDataStreamer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import net.liftweb.json.Printer
3434
object TreemapDataStreamer {
3535
private val Json = new JsonFactory
3636

37-
private def writeJson(jg: JsonGenerator)(node: TreeNode) {
37+
private def writeJson(jg: JsonGenerator)(node: TreeNodeData) {
3838
jg writeFieldName node.id.toString
3939
jg.writeStartObject
4040

0 commit comments

Comments
 (0)