refactored plan.scala and its ReadWriteWeb class in order to seperate more cleanly the different parts of the plan. This makes it easier to wrap the right part of the plan with an access control wrapper. It also makes it a little bit easier to understand. Pursuing this line of thinking could also allow more flexibility later. Also fixed a nullpointer exception, which should go in a different patch, but I am not good enough at mercurial. webid
authorHenry Story <henry.story@bblfish.net>
Mon, 17 Oct 2011 12:52:37 +0200
branchwebid
changeset 84 cf0d0d460b06
parent 83 642d8a1b35d5
child 85 8a84fe5eec71
refactored plan.scala and its ReadWriteWeb class in order to seperate more cleanly the different parts of the plan. This makes it easier to wrap the right part of the plan with an access control wrapper. It also makes it a little bit easier to understand. Pursuing this line of thinking could also allow more flexibility later. Also fixed a nullpointer exception, which should go in a different patch, but I am not good enough at mercurial.
src/main/scala/Filesystem.scala
src/main/scala/ReadWriteWebMain.scala
src/main/scala/auth/Authz.scala
src/main/scala/plan.scala
--- a/src/main/scala/Filesystem.scala	Sun Oct 16 22:47:37 2011 +0200
+++ b/src/main/scala/Filesystem.scala	Mon Oct 17 12:52:37 2011 +0200
@@ -51,6 +51,7 @@
           case RDFRepr(rdfLang) => rdfLang
           case _ => lang
         }
+        case _ => lang
       }
       if (fileOnDisk.exists()) {
         val fis = new FileInputStream(fileOnDisk)
--- a/src/main/scala/ReadWriteWebMain.scala	Sun Oct 16 22:47:37 2011 +0200
+++ b/src/main/scala/ReadWriteWebMain.scala	Mon Oct 17 12:52:37 2011 +0200
@@ -1,6 +1,6 @@
 package org.w3.readwriteweb
 
-import auth.{RDFAuthZ, AuthZ, X509view}
+import auth.{RDFAuthZ, X509view}
 import org.w3.readwriteweb.util._
 
 import unfiltered.jetty._
--- a/src/main/scala/auth/Authz.scala	Sun Oct 16 22:47:37 2011 +0200
+++ b/src/main/scala/auth/Authz.scala	Mon Oct 17 12:52:37 2011 +0200
@@ -25,14 +25,14 @@
 
 import unfiltered.filter.Plan
 import unfiltered.request._
-import unfiltered.response.Unauthorized
 import collection.JavaConversions._
 import javax.security.auth.Subject
-import javax.servlet.http.HttpServletRequest
 import java.net.URL
 import org.w3.readwriteweb.{Resource, ResourceManager, WebCache}
 import com.hp.hpl.jena.query.{QueryExecutionFactory, QueryExecution, QuerySolutionMap, QueryFactory}
 import sun.management.resources.agent
+import unfiltered.response.{ResponseFunction, Unauthorized}
+import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
 
 
 /**
@@ -73,16 +73,18 @@
 }
 
 object NullAuthZ extends AuthZ {
-  def subject(req: NullAuthZ.HSRequest) = null
+  def subject(req: NullAuthZ.Req) = null
 
   def guard(m: Method, path: String) = null
 
-  override def protect(in: Plan.Intent) = in
+  override def protect(in: Req=>Res) = in
 }
 
 
 abstract class AuthZ {
-  type HSRequest = HttpRequest[HttpServletRequest]
+  type Req = HttpRequest[HttpServletRequest]
+  type Res = ResponseFunction[HttpServletResponse]
+  
   // I need a guard
   //   - in order to be able to have different implementations, but subclassing could do to
   //   - the guard should get the information from the file system or the authdb, so it should know where those are
@@ -90,17 +92,16 @@
   // I will need a web cache to get the subject
 
 
-  def protect(in: Plan.Intent): Plan.Intent = {
-    req: HSRequest => req match {
-      case HttpMethod(method) & Path(path) if guard(method, path).allow(() => subject(req)) => in(req)
+  def protect(in: Req=>Res): Req=>Res =  {
+      case req @ HttpMethod(method) & Path(path) if guard(method, path).allow(() => subject(req)) => in(req)
       case _ => Unauthorized
     }
-  }
+  
 
-  def subject(req: HSRequest): Option[Subject]
+  protected def subject(req: Req): Option[Subject]
 
   /** create the guard defined in subclass */
-  def guard(m: Method, path: String): Guard
+  protected def guard(m: Method, path: String): Guard
 
   abstract class Guard(m: Method, path: String) {
 
@@ -118,7 +119,7 @@
   import AuthZ.x509toSubject
   implicit val cache : WebCache = webCache
 
-  def subject(req: HSRequest) = req match {
+  def subject(req: Req) = req match {
     case X509Claim(claim) => Option(claim)
     case _ => None
   }
--- a/src/main/scala/plan.scala	Sun Oct 16 22:47:37 2011 +0200
+++ b/src/main/scala/plan.scala	Mon Oct 17 12:52:37 2011 +0200
@@ -3,9 +3,6 @@
 import auth.{AuthZ, NullAuthZ}
 import org.w3.readwriteweb.util._
 
-import unfiltered.request._
-import unfiltered.response._
-
 import scala.io.Source
 import java.net.URL
 
@@ -20,6 +17,10 @@
               QueryTypeDescribe => DESCRIBE}
 
 import scalaz._
+import unfiltered.filter.Plan
+import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
+import unfiltered.request._
+import unfiltered.response._
 
 //object ReadWriteWeb {
 //
@@ -30,17 +31,40 @@
 //}
 
 class ReadWriteWeb(rm: ResourceManager, implicit val authz: AuthZ = NullAuthZ)  {
-  
+  // a few type short cuts to make it easier to reason with the code here
+  // one may want to generalise this code so that it does not depend so strongly on servlets.
+  type Req = HttpRequest[HttpServletRequest]
+  type Res = ResponseFunction[HttpServletResponse]
+
   val logger: Logger = LoggerFactory.getLogger(this.getClass)
 
-  /** I believe some documentation is needed here, as many different tricks
-   *  are used to make this code easy to read and still type-safe
-   *  
-   *  Planify.apply takes an Intent, which is defined in Cycle by
-   *  type Intent [-A, -B] = PartialFunction[HttpRequest[A], ResponseFunction[B]]
-   *  the corresponding syntax is: case ... => ...
-   *  
-   *  this code makes use of ScalaZ Validation. For example of how to use it, see
+
+ /**
+  *  The Servlet Filter for the ReadWriteWeb
+  *
+  *  Planify.apply takes an Intent, which is defined in Cycle by
+  *  type Intent [-A, -B] = PartialFunction[HttpRequest[A], ResponseFunction[B]]
+  *  the corresponding syntax is: case ... => ...
+  */
+  //bblfish: I don't understand why this has to be lazy, but if it is not, it the plan ends up throwing a null
+  //pointer exception.
+  lazy val plan = unfiltered.filter.Planify(intent)
+
+  /**
+   * The partial function that if triggered sends to the readwrite web code.
+   * It wraps the ReadWriteWeb function with the AuthZ passed in the argument
+   * ( Note that we don't want to protect this intent, since that would be to apply the security to all other applications,
+   * many of which may want different authorization implementations )
+   */
+  val intent : Plan.Intent = {
+      case req @ Path(path) if path startsWith rm.basePath => authz.protect(readWriteWebIntent)(req)
+  }
+
+  /**
+   * The core ReadWrite web function
+   * ( This is not a partial function and so is not a Plan.Intent )
+   *
+   *  This code makes use of ScalaZ Validation. For example of how to use it, see
    *  http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html
    *  
    *  the Resource abstraction returns Validation[Throwable, ?something]
@@ -55,99 +79,100 @@
    *  At last, Validation[ResponseFunction, ResponseFuntion] is exposed as a ResponseFunction
    *  through another implicit conversion. It saves us the call to the Validation.fold() method
    */
-  val plan = unfiltered.filter.Planify {
-    authz.protect {
-    case req @ Path(path) if path startsWith rm.basePath => {
-      val Authoritative(uri, representation) = req
-      val r: Resource = rm.resource(uri)
-      req match {
-        case GET(_) if representation == HTMLRepr => {
-          val source = Source.fromFile("src/main/resources/skin.html")("UTF-8")
-          val body = source.getLines.mkString("\n")
-          Ok ~> ViaSPARQL ~> ContentType("text/html") ~> ResponseString(body)
-        }
-        case GET(_) | HEAD(_) =>
-          for {
-            model <- r.get() failMap { x => NotFound }
-            lang = representation match {
-              case RDFRepr(l) => l
-              case _ => Lang.default
-            }
-          } yield {
-            req match {
-              case GET(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
-              case HEAD(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType)
+  val readWriteWebIntent =  (req: Req) => {
+
+          val Authoritative(uri, representation) = req
+          val r: Resource = rm.resource(uri)
+          val res: Res = req match {
+            case GET(_) if representation == HTMLRepr => {
+              val source = Source.fromFile("src/main/resources/skin.html")("UTF-8")
+              val body = source.getLines.mkString("\n")
+              Ok ~> ViaSPARQL ~> ContentType("text/html") ~> ResponseString(body)
             }
-          }
-        case PUT(_) & RequestLang(lang) if representation == DirectoryRepr => {
-          for {
-            bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
-            _ <- r.createDirectory(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
-          } yield Created
-        }
-        case PUT(_) & RequestLang(lang) =>
-          for {
-            bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
-            _ <- r.save(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
-          } yield Created
-        case PUT(_) =>
-          BadRequest ~> ResponseString("Content-Type MUST be one of: " + Lang.supportedAsString)
-        case POST(_) & RequestContentType(ct) if Post.supportContentTypes contains ct => {
-          Post.parse(Body.stream(req), uri, ct) match {
-            case PostUnknown => {
-              logger.info("Couldn't parse the request")
-              BadRequest ~> ResponseString("You MUST provide valid content for given Content-Type: " + ct)
-            }
-            case PostUpdate(update) => {
-              logger.info("SPARQL UPDATE:\n" + update.toString())
+            case GET(_) | HEAD(_) =>
               for {
-                model <- r.get() failMap { t => NotFound }
-                // TODO: we should handle an error here
-                _ = UpdateAction.execute(update, model)
-                _ <- r.save(model) failMap { t =>  InternalServerError ~> ResponseString(t.getStackTraceString)}
-              } yield Ok
-            }
-            case PostRDF(diffModel) => {
-              logger.info("RDF content:\n" + diffModel.toString())
+                model <- r.get() failMap { x => NotFound }
+                lang = representation match {
+                  case RDFRepr(l) => l
+                  case _ => Lang.default
+                }
+              } yield {
+                req match {
+                  case GET(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+                  case HEAD(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType)
+                }
+              }
+            case PUT(_) & RequestLang(lang) if representation == DirectoryRepr => {
               for {
-                model <- r.get() failMap { t => NotFound }
-                // TODO: we should handle an error here
-                _ = model.add(diffModel)
-                _ <- r.save(model) failMap { t =>  InternalServerError ~> ResponseString(t.getStackTraceString)}
-              } yield Ok
+                bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
+                _ <- r.createDirectory(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
+              } yield Created
             }
-            case PostQuery(query) => {
-              logger.info("SPARQL Query:\n" + query.toString())
-              lazy val lang = RequestLang(req) getOrElse Lang.default
+            case PUT(_) & RequestLang(lang) =>
               for {
-                model <- r.get() failMap { t => NotFound }
-              } yield {
-                val qe: QueryExecution = QueryExecutionFactory.create(query, model)
-                query.getQueryType match {
-                  case SELECT =>
-                    Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execSelect())
-                  case ASK =>
-                    Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execAsk())
-                  case CONSTRUCT => {
-                    val result: Model = qe.execConstruct()
-                    Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
-                  }
-                  case DESCRIBE => {
-                    val result: Model = qe.execDescribe()
-                    Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+                bodyModel <- modelFromInputStream(Body.stream(req), uri, lang) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
+                _ <- r.save(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
+              } yield Created
+            case PUT(_) =>
+              BadRequest ~> ResponseString("Content-Type MUST be one of: " + Lang.supportedAsString)
+            case POST(_) & RequestContentType(ct) if Post.supportContentTypes contains ct => {
+              Post.parse(Body.stream(req), uri, ct) match {
+                case PostUnknown => {
+                  logger.info("Couldn't parse the request")
+                  BadRequest ~> ResponseString("You MUST provide valid content for given Content-Type: " + ct)
+                }
+                case PostUpdate(update) => {
+                  logger.info("SPARQL UPDATE:\n" + update.toString())
+                  for {
+                    model <- r.get() failMap { t => NotFound }
+                    // TODO: we should handle an error here
+                    _ = UpdateAction.execute(update, model)
+                    _ <- r.save(model) failMap { t =>  InternalServerError ~> ResponseString(t.getStackTraceString)}
+                  } yield Ok
+                }
+                case PostRDF(diffModel) => {
+                  logger.info("RDF content:\n" + diffModel.toString())
+                  for {
+                    model <- r.get() failMap { t => NotFound }
+                    // TODO: we should handle an error here
+                    _ = model.add(diffModel)
+                    _ <- r.save(model) failMap { t =>  InternalServerError ~> ResponseString(t.getStackTraceString)}
+                  } yield Ok
+                }
+                case PostQuery(query) => {
+                  logger.info("SPARQL Query:\n" + query.toString())
+                  lazy val lang = RequestLang(req) getOrElse Lang.default
+                  for {
+                    model <- r.get() failMap { t => NotFound }
+                  } yield {
+                    val qe: QueryExecution = QueryExecutionFactory.create(query, model)
+                    query.getQueryType match {
+                      case SELECT =>
+                        Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execSelect())
+                      case ASK =>
+                        Ok ~> ContentType("application/sparql-results+xml") ~> ResponseResultSet(qe.execAsk())
+                      case CONSTRUCT => {
+                        val result: Model = qe.execConstruct()
+                        Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+                      }
+                      case DESCRIBE => {
+                        val result: Model = qe.execDescribe()
+                        Ok ~> ContentType(lang.contentType) ~> ResponseModel(model, uri, lang)
+                      }
+                    }
                   }
                 }
               }
             }
+            case POST(_) =>
+              BadRequest ~> ResponseString("Content-Type MUST be one of: " + Post.supportedAsString)
+            case _ => MethodNotAllowed ~> Allow("GET", "PUT", "POST")
           }
+          res
         }
-        case POST(_) =>
-          BadRequest ~> ResponseString("Content-Type MUST be one of: " + Post.supportedAsString)
-        case _ => MethodNotAllowed ~> Allow("GET", "PUT", "POST")
-      }
-    }
-  }
-
-  }
+      
+    
+    
+  
 
 }
\ No newline at end of file