merge
authorTim Berners-Lee <timbl+hg@w3.org>
Wed, 28 Sep 2011 13:44:48 -0400
changeset 38 38367717b140
parent 37 f2b27056e1a3 (current diff)
parent 35 bc98e70afb56 (diff)
child 39 3f2d3842819e
merge
--- a/project/build.scala	Thu Sep 01 20:25:12 2011 -0400
+++ b/project/build.scala	Wed Sep 28 13:44:48 2011 -0400
@@ -15,6 +15,7 @@
   val jena = "com.hp.hpl.jena" % "jena" % "2.6.4"
   val arq = "com.hp.hpl.jena" % "arq" % "2.8.8"
   val grizzled = "org.clapper" %% "grizzled-scala" % "1.0.7" % "test"
+  val scalaz = "org.scalaz" %% "scalaz-core" % "6.0.2"
 }
 
 // some usefull repositories
@@ -74,6 +75,7 @@
       libraryDependencies += arq,
       libraryDependencies += antiXML,
       libraryDependencies += grizzled,
+      libraryDependencies += scalaz,
       jarName in Assembly := "read-write-web.jar"
     )
 
--- a/src/main/scala/Main.scala	Thu Sep 01 20:25:12 2011 -0400
+++ b/src/main/scala/Main.scala	Wed Sep 28 13:44:48 2011 -0400
@@ -19,6 +19,9 @@
 import Query.{QueryTypeSelect => SELECT, QueryTypeAsk => ASK,
               QueryTypeConstruct => CONSTRUCT, QueryTypeDescribe => DESCRIBE}
 
+import scalaz._
+import Scalaz._
+
 import org.w3.readwriteweb.util._
 
 class ReadWriteWeb(rm:ResourceManager) {
@@ -30,8 +33,30 @@
     accept == Some("text/html") || accept == Some("application/xhtml+xml")
   }
   
+  /** 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 the Validation monad. 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]
+   *  we use the for monadic constructs.
+   *  Everything construct are mapped to Validation[ResponseFunction, ResponseFuntion],
+   *  the left value always denoting the failure. Hence, the rest of the for-construct
+   *  is not evaluated, but let the reader of the code understand clearly what's happening.
+   *  
+   *  This mapping is made possible with the failMap method. I couldn't find an equivalent
+   *  in the ScalaZ API so I made my own through an implicit.
+   *  
+   *  At last, Validation[ResponseFunction, ResponseFuntion] is exposed as a ResponseFunction
+   *  through another implicit conversion. It saves us the call to the Validation.lift() method
+   */
   val read = unfiltered.filter.Planify {
-    case req @ Path(path) if path startsWith (rm.basePath) => {
+    case req @ Path(path) if path startsWith rm.basePath => {
       val baseURI = req.underlying.getRequestURL.toString
       val r:Resource = rm.resource(new URL(baseURI))
       req match {
@@ -41,55 +66,50 @@
           Ok ~> ViaSPARQL ~> ContentType("text/html") ~> ResponseString(body)
         }
         case GET(_) | HEAD(_) =>
-          try {
-            val model:Model = r.get()
-            val encoding = RDFEncoding(req)
+          for {
+            model <- r.get() failMap { x => NotFound }
+            encoding = RDFEncoding(req)
+          } yield {
             req match {
               case GET(_) => Ok ~> ViaSPARQL ~> ContentType(encoding.toContentType) ~> ResponseModel(model, baseURI, encoding)
               case HEAD(_) => Ok ~> ViaSPARQL ~> ContentType(encoding.toContentType)
             }
-          } catch {
-            case fnfe:FileNotFoundException => NotFound
-            case t:Throwable => {
-              req match {
-                case GET(_) => InternalServerError ~> ViaSPARQL
-                case HEAD(_) => InternalServerError ~> ViaSPARQL ~> ResponseString(t.getStackTraceString)
-              }
-            }
           }
         case PUT(_) =>
-          try {
-            val bodyModel = modelFromInputStream(Body.stream(req), baseURI)
-            r.save(bodyModel)
-            Created
-          } catch {
-            case t:Throwable => InternalServerError ~> ResponseString(t.getStackTraceString)
-          }
-        case POST(_) =>
-          try {
-            Post.parse(Body.stream(req), baseURI) match {
-              case PostUnknown => {
-                logger.info("Couldn't parse the request")
-                BadRequest ~> ResponseString("You MUST provide valid content for either: SPARQL UPDATE, SPARQL Query, RDF/XML, TURTLE")
-              }
-              case PostUpdate(update) => {
-                logger.info("SPARQL UPDATE:\n" + update.toString())
-                val model = r.get()
-                UpdateAction.execute(update, model)
-                r.save(model)
-                Ok
-              }
-              case PostRDF(diffModel) => {
-                logger.info("RDF content:\n" + diffModel.toString())
-                val model = r.get()
-                model.add(diffModel)
-                r.save(model)
-                Ok
-              }
-              case PostQuery(query) => {
-                logger.info("SPARQL Query:\n" + query.toString())
-                lazy val encoding = RDFEncoding(req)
-                val model:Model = r.get()
+          for {
+            bodyModel <- modelFromInputStream(Body.stream(req), baseURI) failMap { t => BadRequest ~> ResponseString(t.getStackTraceString) }
+            _ <- r.save(bodyModel) failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
+          } yield Created
+        case POST(_) => {
+          Post.parse(Body.stream(req), baseURI) match {
+            case PostUnknown => {
+              logger.info("Couldn't parse the request")
+              BadRequest ~> ResponseString("You MUST provide valid content for either: SPARQL UPDATE, SPARQL Query, RDF/XML, TURTLE")
+            }
+            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 encoding = RDFEncoding(req)
+              for {
+                model <- r.get() failMap { t => NotFound }
+              } yield {
                 val qe:QueryExecution = QueryExecutionFactory.create(query, model)
                 query.getQueryType match {
                   case SELECT =>
@@ -107,10 +127,8 @@
                 }
               }
             }
-          } catch {
-            case fnfe:FileNotFoundException => NotFound
-            case t:Throwable => InternalServerError ~> ResponseString(t.getStackTraceString)
           }
+        }
         case _ => MethodNotAllowed ~> Allow("GET", "PUT", "POST")
       }
     }
--- a/src/main/scala/Post.scala	Thu Sep 01 20:25:12 2011 -0400
+++ b/src/main/scala/Post.scala	Wed Sep 28 13:44:48 2011 -0400
@@ -18,8 +18,13 @@
 case class PostQuery(query:Query) extends Post
 case object PostUnknown extends Post
 
+import scalaz._
+import Scalaz._
+
 object Post {
   
+  val logger:Logger = LoggerFactory.getLogger(this.getClass)
+
   def parse(is:InputStream, baseURI:String):Post = {
     val source = Source.fromInputStream(is, "UTF-8")
     val s = source.getLines.mkString("\n")
@@ -28,25 +33,23 @@
   
   def parse(s:String, baseURI:String):Post = {
     val reader = new StringReader(s)
-    try {
+    def postUpdate =
       try {
         val update:UpdateRequest = UpdateFactory.create(s, baseURI)
-        PostUpdate(update)      
+        PostUpdate(update).success
       } catch {
-        case qpe:QueryParseException =>
-          try {
-            val model = modelFromString(s, baseURI)
-            PostRDF(model)
-          } catch {
-            case je:JenaException => {
-              val query = QueryFactory.create(s)
-              PostQuery(query)
-            }
-          }
+        case qpe:QueryParseException => qpe.fail
       }
-    } catch {
-      case _ => PostUnknown
-    }
+    def postRDF =
+      modelFromString(s, baseURI) flatMap { model => PostRDF(model).success }
+    def postQuery =
+      try {
+        val query = QueryFactory.create(s)
+        PostQuery(query).success
+      } catch {
+        case qe:QueryException => qe.fail
+      }
+    postUpdate | (postRDF | (postQuery | PostUnknown))
   }
   
 }
--- a/src/main/scala/Resource.scala	Thu Sep 01 20:25:12 2011 -0400
+++ b/src/main/scala/Resource.scala	Wed Sep 28 13:44:48 2011 -0400
@@ -10,14 +10,19 @@
 
 import org.w3.readwriteweb.util._
 
+import scalaz._
+import Scalaz._
+
+import _root_.scala.sys.error
+
 trait ResourceManager {
   def basePath:String
   def sanityCheck():Boolean
   def resource(url:URL):Resource
 }
 trait Resource {
-  def get():Model
-  def save(model:Model):Unit
+  def get():Validation[Throwable, Model]
+  def save(model:Model):Validation[Throwable, Unit]
 }
 
 class Filesystem(baseDirectory:File, val basePath:String, val lang:String = "RDF/XML-ABBREV")(implicit mode:RWWMode) extends ResourceManager {
@@ -38,33 +43,36 @@
       logger.debug("Create file %s with success: %s" format (fileOnDisk.getAbsolutePath, r.toString))
     }
     
-    def get():Model = {
+    def get():Validation[Throwable, Model] = {
       val m = ModelFactory.createDefaultModel()
       if (fileOnDisk.exists()) {
         val fis = new FileInputStream(fileOnDisk)
         try {
           m.read(fis, url.toString)
         } catch {
-          case je:JenaException => sys.error("File %s was either empty or corrupted: considered as empty graph" format fileOnDisk.getAbsolutePath)
+          case je:JenaException => error("File %s was either empty or corrupted: considered as empty graph" format fileOnDisk.getAbsolutePath)
         }
         fis.close()
-        m
+        m.success
       } else {
         mode match {
-          case AllResourcesAlreadyExist => m
-          case ResourcesDontExistByDefault => throw new FileNotFoundException
+          case AllResourcesAlreadyExist => m.success
+          case ResourcesDontExistByDefault => new FileNotFoundException().fail
       }
       }
     }
     
-    def save(model:Model):Unit = {
-      createFileOnDisk()
-      val fos = new FileOutputStream(fileOnDisk)
-      val writer = model.getWriter("RDF/XML-ABBREV")
-      writer.write(model, fos, url.toString)
-      fos.close()
-    }
-    
+    def save(model:Model):Validation[Throwable, Unit] =
+      try {
+        createFileOnDisk()
+        val fos = new FileOutputStream(fileOnDisk)
+        val writer = model.getWriter("RDF/XML-ABBREV")
+        writer.write(model, fos, url.toString)
+        fos.close().success
+      } catch {
+        case t => t.fail
+      }
+
   }
   
 }
--- a/src/main/scala/util.scala	Thu Sep 01 20:25:12 2011 -0400
+++ b/src/main/scala/util.scala	Wed Sep 28 13:44:48 2011 -0400
@@ -9,6 +9,11 @@
 import java.io._
 import scala.io.Source
 
+import scalaz._
+import Scalaz._
+
+import _root_.scala.sys.error
+
 import org.slf4j.{Logger, LoggerFactory}
 
 import com.hp.hpl.jena.rdf.model._
@@ -49,6 +54,11 @@
   
 }
 
+trait ValidationW[E, S] {
+  val validation:Validation[E, S]
+  def failMap[EE](f:E => EE):Validation[EE, S] = validation.fail map f validation
+}
+
 package object util {
   
   val defaultLang = "RDF/XML-ABBREV"
@@ -78,21 +88,38 @@
       }
   }
 
-  def modelFromInputStream(is:InputStream, base:String, lang:String = "RDF/XML-ABBREV"):Model = {
-    val m = ModelFactory.createDefaultModel()
-    m.read(is, base, lang)
-    m
-  }
+  def modelFromInputStream(
+      is:InputStream,
+      base:String,
+      lang:String = "RDF/XML-ABBREV"):Validation[Throwable, Model] =
+    try {
+      val m = ModelFactory.createDefaultModel()
+      m.read(is, base, lang)
+      m.success
+    } catch {
+      case t => t.fail
+    }
   
-  def modelFromString(s:String, base:String, lang:String = "RDF/XML-ABBREV"):Model = {
-    val reader = new StringReader(s)
-    val m = ModelFactory.createDefaultModel()
-    m.read(reader, base, lang)
-    m
-  }
+  def modelFromString(s:String,
+      base:String,
+      lang:String = "RDF/XML-ABBREV"):Validation[Throwable, Model] =
+    try {
+      val reader = new StringReader(s)
+      val m = ModelFactory.createDefaultModel()
+      m.read(reader, base, lang)
+      m.success
+    } catch {
+      case t => t.fail
+    }
 
+  implicit def wrapValidation[E, S](v:Validation[E,S]):ValidationW[E, S] =
+    new ValidationW[E, S] { val validation = v }
+  
+  implicit def unwrap[E, F <: E, S <: E](v:Validation[F,S]):E = v.fold(e => e, s => s)
+  
 }
 
+
 import java.io.{File, FileWriter}
 import java.util.jar._
 import scala.collection.JavaConversions._
@@ -100,7 +127,6 @@
 import java.net.{URL, URLDecoder}
 import org.slf4j.{Logger, LoggerFactory}
 
-
 /** useful stuff to read resources from the classpath */
 object MyResourceManager {
   
@@ -132,7 +158,7 @@
         } }
         entries filterNot { _.isEmpty } toList
       } else
-        sys.error("Cannot list files for URL "+dirURL);
+        error("Cannot list files for URL "+dirURL);
     }
   }
   
--- a/src/test/scala/Test.scala	Thu Sep 01 20:25:12 2011 -0400
+++ b/src/test/scala/Test.scala	Wed Sep 28 13:44:48 2011 -0400
@@ -50,7 +50,7 @@
 </rdf:RDF>
 """
   
-  val initialModel = modelFromString(joeRDF, joeBaseURI)
+  val initialModel = modelFromString(joeRDF, joeBaseURI).toOption.get
 
   "a GET on a URL that does not exist" should {
     "return a 404" in {
@@ -127,7 +127,7 @@
 </rdf:RDF>
 """  
     
-  val expectedFinalModel = modelFromString(finalRDF, joeBaseURI)
+  val expectedFinalModel = modelFromString(finalRDF, joeBaseURI).toOption.get
 
   "POSTing an RDF document to Joe's URI" should {
     "succeed" in {
@@ -195,6 +195,13 @@
       statusCode must_== 400
     }
   }
+  
+  """PUTting not-valid RDF to Joe's URI""" should {
+    "return a 400 Bad Request" in {
+      val statusCode = Http.when(_ == 400)(joe.put("that's bouleshit") get_statusCode)
+      statusCode must_== 400
+    }
+  }
 
   """a DELETE request""" should {
     "not be supported yet" in {
--- a/src/test/scala/utiltest.scala	Thu Sep 01 20:25:12 2011 -0400
+++ b/src/test/scala/utiltest.scala	Wed Sep 28 13:44:48 2011 -0400
@@ -40,7 +40,7 @@
   class RequestW(req:Request) {
 
     def as_model(base:String, lang:String = "RDF/XML-ABBREV"):Handler[Model] =
-      req >> { is => modelFromInputStream(is, base, lang) }
+      req >> { is => modelFromInputStream(is, base, lang).toOption.get }
 
     def post(body:String):Request =
       (req <<< body).copy(method="POST")