Created
December 29, 2011 02:42
-
-
Save diosmosis/1531357 to your computer and use it in GitHub Desktop.
Scala Experiment Iteration 1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scala.actors.Actor | |
import scala.actors.Actor._ | |
import scala.collection.immutable.HashMap | |
import scala.collection.mutable.{HashMap => MutableHashMap} | |
/** | |
* The type passed to RouterActionInvokers. | |
* | |
* The 'message' member can be anything. | |
*/ | |
case class InvokerInfo(callbackName: String, path: Array[String], message: Object) | |
/** | |
* Actor used to invoke route actions. When path is successfully routed, its | |
* callback is executed in an Actor. | |
*/ | |
class RouterActionInvoker(id: String, callbacks: Map[String, (Array[String]) => Unit]) extends Actor { | |
/** | |
* Prints out some metadata & calls the necessary callback. | |
*/ | |
def act() = { | |
while (true) { | |
receive { | |
case InvokerInfo(callbackName, path, message) => | |
println() | |
println("Handling '" + callbackName + "' in Invoker(ID = " + id + ")") | |
println(" Data passed: " + message.toString()) | |
callbacks(callbackName)(path) | |
} | |
} | |
} | |
} | |
/** Represents a segment in a router's tree of valid paths. Describes exactly what | |
* path segments are allowed to come after this one. | |
*/ | |
class RouteTreeNode(pathSegment:String) { | |
/** The segment this node represents. */ | |
private val segment = pathSegment | |
/** The unique name of the callback associated with this node. | |
* | |
* If this member is not null, then the path that leads to this node is a | |
* valid path. | |
*/ | |
var callbackName:String = null | |
/** The child nodes representing possible paths this segment can lead to. */ | |
private val children:MutableHashMap[String, RouteTreeNode] = new MutableHashMap() | |
/** Adds the child to this node's list of children. | |
* | |
* @param child The child node to add. If there already is a child node w/ the | |
* same segment as this node, it is overwritten. | |
* @return returns the child node for convenience. | |
*/ | |
def addChild(child:RouteTreeNode):RouteTreeNode = { | |
children(child.segment) = child | |
return child | |
} | |
/** Returns the child node with the specified segment, or null if there is none. | |
* | |
* @param segment The path segment. | |
* @return The child node associated with segment or null. | |
*/ | |
def getChild(segment:String):RouteTreeNode = { | |
return children.getOrElse(segment, null) | |
} | |
} | |
/** The default actor picking algorithm used by Router. | |
* | |
* Cycles through the list of actors so each new route callback is handled by | |
* the next actor. | |
*/ | |
class ActorCarousel(actorCount:Int) extends Function1[String, Int] { | |
private var currentActor = 0 | |
def apply(callbackName:String):Int = { | |
val result = currentActor | |
currentActor = (currentActor + 1) % actorCount | |
return result | |
} | |
} | |
/** Maintains and traverses a tree of "/my/path/to/something"-like paths. | |
* | |
* A Router will associate URL paths, like /this/is/a/path, with callbacks and | |
* attempt to invoke these callbacks when given an arbitrary path. | |
* | |
* Router will allow you to use wildcards in place for path segments. For | |
* example, /this/:will/match will match both "/this/will/match" and | |
* "/this/abc/match". | |
* | |
* The initialize method must be called before any routing is done and after | |
* all routes are added. | |
*/ | |
class Router(actorCount:Int) { | |
/** The root node of the Router's route tree. Holds every valid path & the | |
* callbacks associated with them. | |
*/ | |
private var routeTree:RouteTreeNode = new RouteTreeNode("") | |
/** The callback invoked when routing fails. */ | |
private var onError:(Array[String]) => Unit = null | |
/** Maps every callback with the string route its associated with. */ | |
private val allCallbacks:MutableHashMap[String, (Array[String]) => Unit] = new MutableHashMap() | |
/** The array of actors used to invoke callbacks. */ | |
private var actors:Array[Actor] = null | |
/** The function used to decide which actor to use when handling a route. | |
* Takes the route path as an argument. | |
*/ | |
private var actorPicker:(String) => Int = new ActorCarousel(actorCount) | |
/** Method to call to finish two-phase construction. Must be called before | |
* doing any routing. | |
*/ | |
def initialize() = { | |
actors = new Array[Actor](actorCount) | |
for (i <- 0 until actorCount) { | |
val actor = new RouterActionInvoker(i.toString(), allCallbacks.toMap) | |
actor.start() | |
actors(i) = actor | |
} | |
} | |
/** Invokes the callback associated with the given path, or the onError | |
* callback if the path is invalid. | |
* | |
* @param path The path to route. | |
* @return True if the route was successful, false if otherwise. | |
*/ | |
def route(path: String, data: Object):Boolean = { | |
if (actors == null || (actors contains null)) { | |
throw new IllegalStateException("Router must be initialized first!"); | |
} | |
// get the path segments & remove the empty segments | |
val segments = path.split("/").filter((s) => s.length > 0) | |
// travel through the route tree, segment by segment | |
var node = routeTree | |
for (segment <- segments) { | |
// look for a child node that matches the segment exactly | |
var child = node.getChild(segment) | |
// no match? try looking for a wildcard | |
if (child == null) { | |
child = node.getChild("*") | |
} | |
// no match? invoke onError | |
if (child == null) { | |
if (onError != null) onError(segments) | |
return false | |
} | |
node = child | |
} | |
// if the specific node has no callback, it is not a valid path end | |
if (node.callbackName == null) { | |
if (onError != null) onError(segments) | |
return false | |
} | |
// pick the actor & send it a message | |
val actor = actors(actorPicker(node.callbackName)) | |
actor ! new InvokerInfo(node.callbackName, segments, data) | |
return true | |
} | |
/** Associates the supplied path with the supplied callback. | |
* | |
* @param path The URL path to associate with. Must be of the format: "/a/sample/path". | |
* Can use wildcards in the format of "/a/path/:wildcard-name". | |
* @param callback The callback to run when a matched path is routed. This callback | |
* will be supplied the path segments when invoked. | |
* @return The Router instance for convenience. | |
*/ | |
def addRoute(path: String, callback: (Array[String]) => Unit):Router = { | |
// get the path segments & remove the empty segments | |
val segments = path.split("/").filter((s) => s.length > 0) | |
// travel the route tree and add missing nodes as they come up | |
var node = routeTree | |
for (segment <- segments) { | |
var realSegment = segment | |
// if segment is a wildcard, replace it with the '*' value. right now, | |
// we don't care about the wildcard's name | |
if (realSegment.startsWith(":")) { | |
realSegment = "*" | |
} | |
val child = node.getChild(realSegment) | |
if (child == null) { | |
node = node.addChild(new RouteTreeNode(realSegment)) | |
} else { | |
node = child | |
} | |
} | |
node.callbackName = path | |
allCallbacks(path) = callback | |
return this | |
} | |
/** Sets the callback that is run when routing fails. | |
* | |
* @param callback The callback. | |
* @return The Router instance for convenience. | |
*/ | |
def setOnError(callback: (Array[String]) => Unit):Router = { | |
onError = callback | |
return this | |
} | |
/** Sets the function used to decide which actor to use when invoking a | |
* route action. | |
*/ | |
def setActorPicker(picker: (String) => Int):Router = { | |
if (picker == null) { | |
throw new IllegalArgumentException("picker cannot be null") | |
} | |
actorPicker = picker | |
return this | |
} | |
} | |
/** Testing singleton. */ | |
object Test { | |
def main(args: Array[String]) = { | |
val router = new Router(3) | |
router.addRoute("/test/route", (path: Array[String]) => println("At /test/route")) | |
.addRoute("/pages/:name", (path: Array[String]) => println("At /pages/:name w/ name=" + path(1))) | |
.addRoute("/things/:with/:widgets", | |
(path: Array[String]) => println("At /things/:with/:widgets w/ with=" + path(1) + " & widgets=" + path(2))) | |
.setOnError((path: Array[String]) => println("Route failed: /" + path.reduceLeft(_ + "/" + _))) | |
router.initialize() | |
val routesToTest = List(Array("/test/route", "no-data1"), | |
Array("/pages/my_page", "no-data2"), | |
Array("/things/w/i", "no-data3"), | |
Array("/pages/my-other-page", "no-data4"), | |
Array("/things/w", "no-data5")) | |
routesToTest.foreach((pair) => router.route(pair(0), pair(1))) | |
} | |
} | |
Test.main(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment