catching up fully with Alex Bertails changes. Added also a bit of mime type guessing in the FileSystem class' get method webid
authorHenry Story <henry.story@bblfish.net>
Sun, 16 Oct 2011 22:47:37 +0200
branchwebid
changeset 83 642d8a1b35d5
parent 82 42e553469cef (current diff)
parent 73 5a8fc47be78b (diff)
child 84 cf0d0d460b06
catching up fully with Alex Bertails changes. Added also a bit of mime type guessing in the FileSystem class' get method
src/main/scala/Authoritative.scala
src/main/scala/Filesystem.scala
src/main/scala/Lang.scala
src/main/scala/ReadWriteWebMain.scala
src/main/scala/Representation.scala
src/main/scala/RequestLang.scala
src/main/scala/Resource.scala
src/main/scala/WebCache.scala
src/main/scala/plan.scala
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/resources/log4j.properties	Sun Oct 16 22:47:37 2011 +0200
@@ -0,0 +1,9 @@
+// Appenders
+log4j.appender.Console=org.apache.log4j.ConsoleAppender
+log4j.appender.Console.layout=org.apache.log4j.PatternLayout
+// Pattern not for use in production ( %C
+log4j.appender.Console.layout.ConversionPattern=%-5p (%F:%L) : %m%n
+
+// Loggers
+log4j.rootLogger=info, Console
+log4j.logger.org.w3.readwriteweb=debug
--- a/src/main/scala/Authoritative.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/main/scala/Authoritative.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -1,58 +1,31 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ *    http://www.opensource.org/licenses/mit-license.html
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in the
+ * Software without restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so, subject to the
+ * following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
 package org.w3.readwriteweb
 
 import unfiltered.request._
 import java.net.URL
 
-sealed trait Representation
-
-object Representation {
-  
-  def fromSuffix(suffix: String): Representation = {
-    suffix match {
-      case "n3" => RDFRepr(N3)
-      case "turtle" | "ttl" => RDFRepr(TURTLE)
-      case "rdf" => RDFRepr(RDFXML)
-      case "htm" | "html" | "xhtml" => HTMLRepr
-      case _ => UnknownRepr
-    }
-  }
-  
-  val htmlCharsets = Set("text/html", "application/xhtml+xml")
-  
-  def acceptsHTML(ct: Iterable[String]) =
-    ! (htmlCharsets & ct.toSet).isEmpty
-  
-  def fromAcceptedContentTypes(ct: Iterable[String]): Representation = {
-    Lang(ct) map RDFRepr.apply getOrElse {
-      if (acceptsHTML(ct))
-        HTMLRepr
-      else
-        UnknownRepr
-    }
-  }
-  
-  /** implements http://www.w3.org/2001/tag/doc/metaDataInURI-31 and http://www.w3.org/2001/tag/doc/mime-respect
-    * 
-    * if there is no known suffix (eg. the URI was already the authoritative one),
-    * inspects the given accepted content types
-    * 
-    * This knows only about the RDF and HTML charsets
-    */
-  def apply(
-      suffixOpt: Option[String],
-      ct: Iterable[String]): Representation = {
-    suffixOpt map fromSuffix match {
-      case None | Some(UnknownRepr) => fromAcceptedContentTypes(ct)
-      case Some(repr) => repr
-    }
-  }
-}
-
-case class RDFRepr(lang: Lang) extends Representation
-case object HTMLRepr extends Representation
-case object UnknownRepr extends Representation
-case object NoRepr extends Representation
-
 object Authoritative {
   
   val r = """^(.*)\.(\w{0,4})$""".r
@@ -61,8 +34,10 @@
     val uri = req.underlying.getRequestURL.toString
     val suffixOpt = uri match {
       case r(_, suffix) => Some(suffix)
+      case _ if uri endsWith "/" => Some("/")
       case _ => None
     }
     Some((new URL(uri), Representation(suffixOpt, Accept(req))))
   }
+
 }
--- a/src/main/scala/Filesystem.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/main/scala/Filesystem.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -7,40 +7,58 @@
 import org.slf4j.{Logger, LoggerFactory}
 import com.hp.hpl.jena.rdf.model._
 import com.hp.hpl.jena.shared.JenaException
-import sys.error
-import scalaz._
+
+import scalaz.{sys => _, _}
 import Scalaz._
 
 class Filesystem(
   baseDirectory: File,
   val basePath: String,
-  val lang: String = "RDF/XML-ABBREV")(mode: RWWMode) extends ResourceManager {
+  val lang: Lang)(mode: RWWMode) extends ResourceManager {
   
-  val logger:Logger = LoggerFactory.getLogger(this.getClass)
-
-  def sanityCheck():Boolean = baseDirectory.exists
-
-  def resource(url:URL):Resource = new Resource {
-    val relativePath:String = url.getPath.replaceAll("^"+basePath.toString+"/?", "")
+  val logger: Logger = LoggerFactory.getLogger(this.getClass)
+  
+  def sanityCheck(): Boolean =
+    baseDirectory.exists && baseDirectory.isDirectory
+  
+  def resource(url: URL): Resource = new Resource {
+    val relativePath: String = url.getPath.replaceAll("^"+basePath.toString+"/?", "")
     val fileOnDisk = new File(baseDirectory, relativePath)
-
-    private def createFileOnDisk():Unit = {
-      // create parent directory if needed
+    
+    private def parentMustExist(): Unit = {
       val parent = fileOnDisk.getParentFile
-      if (! parent.exists) println(parent.mkdirs)
+      if (! parent.exists) sys.error("Parent directory %s does not exist" format parent.getAbsolutePath)
+      if (! parent.isDirectory) sys.error("Parent %s is not a directory" format parent.getAbsolutePath)
+    }
+    
+    private def createDirectoryOnDisk(): Unit = {
+      parentMustExist()
+      val r = fileOnDisk.mkdir()
+      if (!r) sys.error("Could not create %s" format fileOnDisk.getAbsolutePath)
+      logger.debug("%s successfully created: %s" format (fileOnDisk.getAbsolutePath, r.toString))
+    }
+    
+    private def createFileOnDisk(): Unit = {
+      parentMustExist()
       val r = fileOnDisk.createNewFile()
-      logger.debug("Create file %s with success: %s" format (fileOnDisk.getAbsolutePath, r.toString))
+      logger.debug("%s successfully created: %s" format (fileOnDisk.getAbsolutePath, r.toString))
     }
-
+    
     def get(): Validation[Throwable, Model] = {
       val model = ModelFactory.createDefaultModel()
+      val guessLang = fileOnDisk.getName match {
+        case Authoritative.r(_,suffix) => Representation.fromSuffix(suffix) match {
+          case RDFRepr(rdfLang) => rdfLang
+          case _ => lang
+        }
+      }
       if (fileOnDisk.exists()) {
         val fis = new FileInputStream(fileOnDisk)
         try {
-          val reader = model.getReader(lang)
+          val reader = model.getReader(guessLang.jenaLang)
           reader.read(model, fis, url.toString)
         } catch {
-          case je:JenaException => error(je.toString)
+          case je: JenaException => throw je
         }
         fis.close()
         model.success
@@ -51,18 +69,29 @@
         }
       }
     }
-
-    def save(model:Model):Validation[Throwable, Unit] =
+    
+    def save(model: Model): Validation[Throwable, Unit] =
       try {
         createFileOnDisk()
         val fos = new FileOutputStream(fileOnDisk)
-        val writer = model.getWriter(lang)
+        val writer = model.getWriter(lang.jenaLang)
         writer.write(model, fos, url.toString)
         fos.close().success
       } catch {
         case t => t.fail
       }
 
+    def createDirectory(model: Model): Validation[Throwable, Unit] =
+      try {
+        createDirectoryOnDisk().success
+//        val fos = new FileOutputStream(fileOnDisk)
+//        val writer = model.getWriter(lang.contentType)
+//        writer.write(model, fos, url.toString)
+//        fos.close().success
+      } catch {
+        case t => t.fail
+      }
+
   }
   
 }
--- a/src/main/scala/Lang.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/main/scala/Lang.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -38,7 +38,7 @@
     case TURTLE => "TURTLE"
     case N3 => "N3"
   }
-  
+
 }
 
 object Lang {
@@ -55,7 +55,7 @@
       case "text/turtle" => Some(TURTLE)
       case "application/rdf+xml" => Some(RDFXML)
       case _ => None
-  }
+  }    
   
   def apply(cts: Iterable[String]): Option[Lang] =
     cts map Lang.apply collectFirst { case Some(lang) => lang }
--- a/src/main/scala/ReadWriteWebMain.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/main/scala/ReadWriteWebMain.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -12,7 +12,7 @@
 import ArgotConverters._
 object ReadWriteWebMain {
 
-  val logger:Logger = LoggerFactory.getLogger(this.getClass)
+  val logger: Logger = LoggerFactory.getLogger(this.getClass)
 
   val postUsageMsg= Some("""
   |PROPERTIES
@@ -31,56 +31,56 @@
   val parser = new ArgotParser("read-write-web",postUsage=postUsageMsg)
 
   val mode = parser.option[RWWMode](List("mode"), "m", "wiki mode: wiki or strict") {
-      (sValue, opt) =>
-        sValue match {
-          case "wiki" => AllResourcesAlreadyExist
-          case "strict" => ResourcesDontExistByDefault
-          case _ => throw new ArgotConversionException("Option %s: must be either wiki or strict" format (opt.name, sValue))
-        }
+    (sValue, opt) =>
+      sValue match {
+        case "wiki" => AllResourcesAlreadyExist
+        case "strict" => ResourcesDontExistByDefault
+        case _ => throw new ArgotConversionException("Option %s: must be either wiki or strict" format (opt.name, sValue))
       }
+    }
 
-    val rdfLanguage = parser.option[String](List("language"), "l", "RDF language: n3, turtle, or rdfxml") {
-      (sValue, opt) =>
-        sValue match {
-          case "n3" => "N3"
-          case "turtle" => "N3"
-          case "rdfxml" => "RDF/XML-ABBREV"
-          case _ => throw new ArgotConversionException("Option %s: must be either n3, turtle or rdfxml" format (opt.name, sValue))
+  val rdfLanguage = parser.option[Lang](List("language"), "l", "RDF language") {
+    (sValue, opt) =>
+      sValue match {
+        case "n3" => N3
+        case "turtle" => TURTLE
+        case "rdfxml" => RDFXML
+        case _ => throw new ArgotConversionException("Option %s: must be either n3, turtle or rdfxml" format (opt.name, sValue))
       }
   }
 
     val httpPort = parser.option[Int]("http", "Port","start the http server on port")
     val httpsPort = parser.option[Int]("https","port","start the https server on port")
 
-    val rootDirectory = parser.parameter[File]("rootDirectory", "root directory", false) {
-      (sValue, opt) => {
-        val file = new File(sValue)
-        if (! file.exists)
-          throw new ArgotConversionException("Option %s: %s must be a valid path" format (opt.name, sValue))
-        else
-          file
+  val rootDirectory = parser.parameter[File]("rootDirectory", "root directory", false) {
+    (sValue, opt) => {
+      val file = new File(sValue)
+      if (! file.exists)
+        throw new ArgotConversionException("Option %s: %s must be a valid path" format (opt.name, sValue))
+      else
+        file
     }
-    }
+  }
 
    implicit val webCache = new WebCache()
 
-   val baseURL = parser.parameter[String]("baseURL", "base URL", false)
+  val baseURL = parser.parameter[String]("baseURL", "base URL", false)
 
   // regular Java main
   def main(args: Array[String]) {
 
     try {
-       parser.parse(args)
-     } catch {
+      parser.parse(args)
+    } catch {
       case e: ArgotUsageException => err.println(e.message); sys.exit(1)
     }
 
     val filesystem =
-        new Filesystem(
-          rootDirectory.value.get,
-          baseURL.value.get,
-          lang=rdfLanguage.value getOrElse "N3")(mode.value getOrElse ResourcesDontExistByDefault)
-
+      new Filesystem(
+        rootDirectory.value.get,
+        baseURL.value.get,
+        lang=rdfLanguage.value getOrElse RDFXML)(mode.value getOrElse ResourcesDontExistByDefault)
+    
     val app = new ReadWriteWeb(filesystem, new RDFAuthZ(webCache,filesystem))
 
     //this is incomplete: we should be able to start both ports.... not sure how to do this yet.
@@ -92,11 +92,11 @@
     // configures and launches a Jetty server
     service.filter(new FilterLogger(logger)).
       context("/public"){ ctx:ContextBuilder =>
-      ctx.resources(ClasspathUtils.fromClasspath("public/").toURI.toURL)
+        ctx.resources(ClasspathUtils.fromClasspath("public/").toURI.toURL)
     }.
       filter(app.plan).
       filter(new X509view().plan).run()
-
+    
   }
 
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/Representation.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ *    http://www.opensource.org/licenses/mit-license.html
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in the
+ * Software without restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so, subject to the
+ * following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package org.w3.readwriteweb
+
+import unfiltered.request._
+import java.net.URL
+
+sealed trait Representation
+
+object Representation {
+  
+  def fromSuffix(suffix: String): Representation = {
+    suffix match {
+      case "n3" => RDFRepr(N3)
+      case "turtle" | "ttl" => RDFRepr(TURTLE)
+      case "rdf" => RDFRepr(RDFXML)
+      case "htm" | "html" | "xhtml" => HTMLRepr
+      case "/" => DirectoryRepr
+      case _ => UnknownRepr
+    }
+  }
+  
+
+  val htmlContentTypes = Set("text/html", "application/xhtml+xml")
+  
+  def acceptsHTML(ct: Iterable[String]) =
+    ! (htmlContentTypes & ct.toSet).isEmpty
+  
+  def fromAcceptedContentTypes(ct: Iterable[String]): Representation = {
+    Lang(ct) map RDFRepr.apply getOrElse {
+      if (acceptsHTML(ct))
+        HTMLRepr
+      else
+        UnknownRepr
+    }
+  }
+  
+  /** implements http://www.w3.org/2001/tag/doc/metaDataInURI-31 and http://www.w3.org/2001/tag/doc/mime-respect
+    * 
+    * if there is no known suffix (eg. the URI was already the authoritative one),
+    * inspects the given accepted content types
+    * 
+    * This knows only about the RDF and HTML charsets
+    */
+  def apply(
+      suffixOpt: Option[String],
+      ct: Iterable[String]): Representation = {
+    suffixOpt map fromSuffix match {
+      case None | Some(UnknownRepr) => fromAcceptedContentTypes(ct)
+      case Some(repr) => repr
+    }
+  }
+}
+
+case class RDFRepr(lang: Lang) extends Representation
+case object HTMLRepr extends Representation
+case object DirectoryRepr extends Representation
+case object UnknownRepr extends Representation
--- a/src/main/scala/RequestLang.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/main/scala/RequestLang.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -1,3 +1,26 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined
+ *    http://www.opensource.org/licenses/mit-license.html
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in the
+ * Software without restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so, subject to the
+ * following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
 package org.w3.readwriteweb
 
 import unfiltered.request._
--- a/src/main/scala/Resource.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/main/scala/Resource.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -16,5 +16,6 @@
 trait Resource {
   def get(): Validation[Throwable, Model]
   def save(model: Model): Validation[Throwable, Unit]
+  def createDirectory(model: Model): Validation[Throwable, Unit]
 }
 
--- a/src/main/scala/WebCache.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/main/scala/WebCache.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -74,6 +74,10 @@
 
     }
 
-    def save(model: Model) = { throw new MethodNotSupportedException("not implemented"); null }
+    // when fetching information from the web creating directories does not make sense
+    //perhaps the resource manager should be split into read/write sections?
+    def save(model: Model) =  throw new MethodNotSupportedException("not implemented")
+
+    def createDirectory(model: Model) =  throw new MethodNotSupportedException("not implemented")
   }
 }
--- a/src/main/scala/plan.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/main/scala/plan.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -21,15 +21,18 @@
 
 import scalaz._
 
-class ReadWriteWeb(rm: ResourceManager, implicit val authz: AuthZ = NullAuthZ) {
+//object ReadWriteWeb {
+//
+//  val defaultHandler: PartialFunction[Throwable, HttpResponse[_]] = {
+//    case t => InternalServerError ~> ResponseString(t.getStackTraceString)
+//  }
+//
+//}
+
+class ReadWriteWeb(rm: ResourceManager, implicit val authz: AuthZ = NullAuthZ)  {
   
   val logger: Logger = LoggerFactory.getLogger(this.getClass)
 
-  def isHTML(accepts: List[String]): Boolean = {
-    val accept = accepts.headOption
-    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
    *  
@@ -58,7 +61,7 @@
       val Authoritative(uri, representation) = req
       val r: Resource = rm.resource(uri)
       req match {
-        case GET(_) & Accept(accepts) if isHTML(accepts) => {
+        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)
@@ -76,6 +79,12 @@
               case HEAD(_) => Ok ~> ViaSPARQL ~> ContentType(lang.contentType)
             }
           }
+        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) }
--- a/src/test/scala/CreateContentSpecs.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/test/scala/CreateContentSpecs.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -5,7 +5,7 @@
 
 import dispatch._
 
-object PutRDFXMLSpec extends FilesystemBased with SomeRDF with SomeURI {
+object PutRDFXMLSpec extends SomePeopleDirectory {
 
   "PUTing an RDF document on Joe's URI (which does not exist yet)" should {
     "return a 201" in {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/test/scala/CreateDirectorySpec.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -0,0 +1,20 @@
+package org.w3.readwriteweb
+
+import org.w3.readwriteweb.util._
+import org.w3.readwriteweb.utiltest._
+
+import dispatch._
+
+object CreateDirSpec extends FilesystemBased with SomeRDF with SomeURI {
+
+  "PUTing an RDF document on /people/" should {
+    "return a 201" in {
+      val httpCode = Http(dirUri.put(RDFXML, rdfxml) get_statusCode)
+      httpCode must_== 201
+    }
+    "create a directory on disk" in {
+      directory must be directory
+    }
+  }
+
+}
--- a/src/test/scala/ReadWriteWebSpecs.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/test/scala/ReadWriteWebSpecs.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -7,6 +7,8 @@
       // access content
       GetStrictModeSpec, GetWikiModeSpec,
       ContentNegociationSpec,
+      // create directory
+      CreateDirSpec,
       // create content
       PutRDFXMLSpec, PostRDFSpec,
       PutInvalidRDFXMLSpec, PostOnNonExistingResourceSpec,
--- a/src/test/scala/util/specs.scala	Sun Oct 16 21:38:18 2011 +0200
+++ b/src/test/scala/util/specs.scala	Sun Oct 16 22:47:37 2011 +0200
@@ -31,13 +31,13 @@
   
   lazy val mode: RWWMode = ResourcesDontExistByDefault
   
-  lazy val language = "RDF/XML-ABBREV"
+  lazy val lang = RDFXML
     
-  lazy val baseURL = "/2007/wiki"
+  lazy val baseURL = "/wiki"
   
   lazy val root = new File(new File(System.getProperty("java.io.tmpdir")), "readwriteweb")
 
-  lazy val resourceManager = new Filesystem(root, baseURL, language)(mode)
+  lazy val resourceManager = new Filesystem(root, baseURL, lang)(mode)
   
   doBeforeSpec {
     if (root.exists) root.deleteRecursively()
@@ -70,15 +70,28 @@
   
   val emptyModel = com.hp.hpl.jena.rdf.model.ModelFactory.createDefaultModel()
   
-  lazy val uri = host / "2007/wiki/people/JoeLambda"
+  lazy val dirUri = host / "wiki/people/"
+  
+  lazy val uri = host / "wiki/people/JoeLambda"
   
   lazy val uriBase = baseURI(uri)
   
+  lazy val directory = new File(root, "people")
+  
   lazy val resourceOnDisk = new File(root, "people/JoeLambda")
   
 }
 
-trait SomeDataInStore extends FilesystemBased with SomeRDF with SomeURI {
+trait SomePeopleDirectory extends SomeRDF with SomeURI {
+  
+  doBeforeSpec {
+    val httpCode = Http(dirUri.put(RDFXML, rdfxml) get_statusCode)
+    httpCode must_== 201
+  }
+  
+}
+
+trait SomeDataInStore extends SomePeopleDirectory {
   
   doBeforeSpec {
     val httpCode = Http(uri.put(RDFXML, rdfxml) get_statusCode)