allow PUT and GET of images webid
authorHenry Story <henry.story@bblfish.net>
Fri, 13 Apr 2012 17:43:15 +0200
branchwebid
changeset 198 014d92d1bb13
parent 197 c6520ef80d5c
child 199 af8ab11f6192
allow PUT and GET of images
src/main/scala/Filesystem.scala
src/main/scala/GraphCache.scala
src/main/scala/Image.scala
src/main/scala/Lang.scala
src/main/scala/ReadWriteWeb.scala
src/main/scala/Representation.scala
src/main/scala/Resource.scala
src/main/scala/auth/WebIdClaim.scala
src/main/scala/netty/ReadWriteWebNetty.scala
--- a/src/main/scala/Filesystem.scala	Wed Apr 11 14:56:25 2012 +0200
+++ b/src/main/scala/Filesystem.scala	Fri Apr 13 17:43:15 2012 +0200
@@ -17,50 +17,54 @@
   baseDirectory: File,
   val basePath: String,
   val lang: Lang)(mode: RWWMode) extends ResourceManager {
-  
+
   val logger: Logger = LoggerFactory.getLogger(this.getClass)
-  
+
   def sanityCheck(): Boolean =
     baseDirectory.exists && baseDirectory.isDirectory
 
   def resource(url: URL): Resource = new Resource {
     def name() = url
-    val relativePath: String = url.getPath.replaceAll("^"+basePath.toString+"/?", "")
+
+    val relativePath: String = url.getPath.replaceAll("^" + basePath.toString + "/?", "")
     val fileOnDisk = new File(baseDirectory, relativePath)
     lazy val parent = fileOnDisk.getParentFile
-    
+
     private def parentMustExist(): Unit = {
       val parent = fileOnDisk.getParentFile
-      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)
+      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))
+      logger.debug("%s successfully created: %s" format(fileOnDisk.getAbsolutePath, r.toString))
     }
-    
+
     private def createFileOnDisk(): Unit = {
       parentMustExist()
       val r = fileOnDisk.createNewFile()
-      logger.debug("%s successfully created: %s" format (fileOnDisk.getAbsolutePath, r.toString))
+      logger.debug("%s successfully created: %s" format(fileOnDisk.getAbsolutePath, r.toString))
     }
-    
+
     def get(unused: CacheControl.Value = CacheControl.CacheFirst): Validation[Throwable, Model] = {
       val model = ModelFactory.createDefaultModel()
 
+      //for files: other possible ontologies to use would be
+      // "Linked Data Basic Profile 1.0" http://www.w3.org/Submission/2012/02/
+      // "the posix ontology" used by data.fm http://www.w3.org/ns/posix/stat#
       if (fileOnDisk.isDirectory) {
         val sioc = "http://rdfs.org/sioc/ns#"
         val dirRes = model.createResource(name.toString)
-        dirRes.addProperty(RDF.`type`,model.createResource(sioc+"Container"))
-        for ( f <- fileOnDisk.listFiles() ) {
-          val furl = new URL(name,f.getName)
-          val item =model.createResource(furl.toString)
-          dirRes.addProperty(model.createProperty(sioc+"container_of"),item)
-          if (f.isDirectory) item.addProperty(RDF.`type`,model.createResource(sioc+"Container"))
-          else item.addProperty(RDF.`type`,model.createResource(sioc+"Item"))
+        dirRes.addProperty(RDF.`type`, model.createResource(sioc + "Container"))
+        for (f <- fileOnDisk.listFiles()) {
+          val furl = new URL(name, f.getName)
+          val item = model.createResource(furl.toString)
+          dirRes.addProperty(model.createProperty(sioc + "container_of"), item)
+          if (f.isDirectory) item.addProperty(RDF.`type`, model.createResource(sioc + "Container"))
+          else item.addProperty(RDF.`type`, model.createResource(sioc + "Item"))
         }
         model.success
       } else {
@@ -89,7 +93,29 @@
         }
       }
     }
-    
+
+    def getStream = try {
+      new BufferedInputStream(new FileInputStream(fileOnDisk)).success
+    } catch {
+      case fe: FileNotFoundException => fe.fail
+      case se: SecurityException => se.fail
+    }
+
+    def putStream(in: InputStream): Validation[Throwable, Unit] = {
+      val out = new FileOutputStream(fileOnDisk)
+      val buf = new Array[Byte](4096)
+      try {
+       val good = Iterator continually in.read(buf) takeWhile (-1 !=) foreach  { bytesRead =>
+          out.write(buf,0,bytesRead)
+        }
+        good.success
+      } catch {
+        case ioe: IOException => ioe.fail
+      }
+
+    }
+
+
     def save(model: Model): Validation[Throwable, Unit] =
       try {
         parent.mkdirs()
@@ -101,7 +127,7 @@
         case t => t.fail
       }
 
-    def createDirectory(model: Model): Validation[Throwable, Unit] =
+    def createDirectory: Validation[Throwable, Unit] =
       try {
         createDirectoryOnDisk().success
 //        val fos = new FileOutputStream(fileOnDisk)
@@ -112,26 +138,27 @@
         case t => t.fail
       }
 
-    def delete : Validation[Throwable, Unit]= try {
-       Files.delete(fileOnDisk.toPath).success
+    def delete: Validation[Throwable, Unit] = try {
+      Files.delete(fileOnDisk.toPath).success
     } catch {
       case e: IOException => e.fail
     }
 
-    def create(): Validation[Throwable, Resource] =  {
-       if (!fileOnDisk.exists())
-         new Throwable("Must first create "+name()).fail
-       else if (!fileOnDisk.isDirectory)
-         new Throwable("Can only create a resource in a directory/collection which this is not "+name()).fail
-       else try {
-         val path = Files.createTempFile(fileOnDisk.toPath,"res",lang.suffix)
-         resource(new URL(name(),path.getFileName.toString)).success
-       } catch {
-         case ioe: IOException => ioe.fail
-       }
+    def create(): Validation[Throwable, Resource] = {
+      if (!fileOnDisk.exists())
+        new Throwable("Must first create " + name()).fail
+      else if (!fileOnDisk.isDirectory)
+        new Throwable("Can only create a resource in a directory/collection which this is not " + name()).fail
+      else try {
+        val path = Files.createTempFile(fileOnDisk.toPath, "res", lang.suffix)
+        resource(new URL(name(), path.getFileName.toString)).success
+      } catch {
+        case ioe: IOException => ioe.fail
+      }
     }
 
-
   }
+
+}
   
-}
+
--- a/src/main/scala/GraphCache.scala	Wed Apr 11 14:56:25 2012 +0200
+++ b/src/main/scala/GraphCache.scala	Fri Apr 13 17:43:15 2012 +0200
@@ -30,13 +30,13 @@
 import scalaz.{Scalaz, Validation}
 import java.util.concurrent.TimeUnit
 import com.google.common.cache.{LoadingCache, CacheLoader, CacheBuilder, Cache}
-import java.io.{File, FileOutputStream}
 import com.weiglewilczek.slf4s.Logging
 import javax.net.ssl.SSLContext
 import org.apache.http.conn.scheme.Scheme
 import java.security.NoSuchAlgorithmException
 import org.apache.http.conn.ssl.{TrustStrategy, SSLSocketFactory}
 import java.security.cert.X509Certificate
+import java.io.{InputStream, File, FileOutputStream}
 
 
 /**
@@ -118,12 +118,15 @@
     //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")
+    def createDirectory =  throw new MethodNotSupportedException("not implemented")
 
     def delete = throw new MethodNotSupportedException("not implemented")
 
     def create() = throw new MethodNotSupportedException("not implemented")
 
+    def getStream = throw new MethodNotSupportedException("not implemented")
+
+    def putStream(in: InputStream) = throw new MethodNotSupportedException("not implemented")
   }
 
   private def getUrl(u: URL) = {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/Image.scala	Fri Apr 13 17:43:15 2012 +0200
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2012 Henry Story (bblfish.net)
+ * under the MIT licence defined at
+ *    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
+
+/**
+ * Image mime types adapted in a hurry from the Lang class.
+ *
+ * It really feels like oneshould have one whole set of mime type classes that cover all the cases.
+ *
+ * @author bblfish
+ * @created 12/04/2012
+ */
+
+sealed trait Image {
+
+  def suffix = this match {
+    case JPEG => ".jpeg"
+    case GIF => ".gif"
+    case PNG => ".png"
+  }
+
+  def contentType = this match {
+    case JPEG => "image/jpeg"
+    case GIF => "image/gif"
+    case PNG => "image/png"
+  }
+
+}
+
+
+object Image {
+
+  val supportedImages = Set(JPEG, GIF, PNG)
+
+  def apply(contentType: String): Option[Image] = {
+    contentType.trim.toLowerCase match {
+      case "image/jpeg" => Some(JPEG)
+      case "image/gif" => Some(GIF)
+      case "image/png" => Some(PNG)
+      case _ => None
+    }
+  }
+
+  def apply(cts: Iterable[String]): Option[Image] =
+    cts map Image.apply collectFirst {
+      case Some(lang) => lang
+    }
+}
+
+case object JPEG extends Image
+case object GIF extends Image
+case object PNG extends Image
--- a/src/main/scala/Lang.scala	Wed Apr 11 14:56:25 2012 +0200
+++ b/src/main/scala/Lang.scala	Fri Apr 13 17:43:15 2012 +0200
@@ -30,7 +30,6 @@
     case HTML => "HTML"
     case XHTML => "XHTML"
   }
-
 }
 
 object Lang {
--- a/src/main/scala/ReadWriteWeb.scala	Wed Apr 11 14:56:25 2012 +0200
+++ b/src/main/scala/ReadWriteWeb.scala	Fri Apr 13 17:43:15 2012 +0200
@@ -1,6 +1,7 @@
 package org.w3.readwriteweb
 
 import auth.{AuthZ, NullAuthZ}
+import netty.ResponseBin
 import org.w3.readwriteweb.util._
 
 import scala.io.Source
@@ -85,6 +86,13 @@
               val body = source.getLines.mkString("\n")
               Ok ~> ViaSPARQL ~> ContentType("text/html") ~> ResponseString(body)
             }
+            case GET(_) if representation.isInstanceOf[ImageRepr] => {
+              for (in <- r.getStream failMap {x => NotFound })
+              yield {
+                val ImageRepr(typ) = representation
+                Ok ~> ContentType(typ.contentType) ~> ResponseBin(in)
+              }
+            }
             case  GET(_) | HEAD(_) =>
               for {
                 model <- r.get() failMap { x => NotFound }
@@ -101,10 +109,14 @@
                 }
                 res ~> ContentLocation( uri.toString ) // without this netty (perhaps jetty too?) sends very weird headers, breaking tests
               }
+            case PUT(_) if representation.isInstanceOf[ImageRepr] => {
+              for (_ <- r.putStream(Body.stream(req)) failMap { t=> InternalServerError ~> ResponseString(t.getStackTraceString)})
+              yield Created
+            }
             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) }
+                _ <- r.createDirectory failMap { t => InternalServerError ~> ResponseString(t.getStackTraceString) }
               } yield Created
             }
             case PUT(_) & RequestLang(lang) =>
@@ -190,9 +202,5 @@
           }
           res
         }
-      
-    
-    
-  
 
 }
--- a/src/main/scala/Representation.scala	Wed Apr 11 14:56:25 2012 +0200
+++ b/src/main/scala/Representation.scala	Fri Apr 13 17:43:15 2012 +0200
@@ -14,6 +14,9 @@
       case "turtle" | "ttl" => RDFRepr(TURTLE)
       case "rdf" => RDFRepr(RDFXML)
       case "htm" | "html" | "xhtml" => HTMLRepr
+      case "jpeg" => ImageRepr(JPEG)
+      case "png" => ImageRepr(PNG)
+      case "gif" => ImageRepr(GIF)
       case "/" => DirectoryRepr
       case _ => UnknownRepr
     }
@@ -21,16 +24,22 @@
   
 
   val htmlContentTypes = Set("text/html", "application/xhtml+xml")
+  val imgageTypes = Set("image/jpeg","image/png","image/gif")
   
   def acceptsHTML(ct: Iterable[String]) =
     ! (htmlContentTypes & ct.toSet).isEmpty
+
+  def acceptsPicture(ct: Iterable[String]) =
+    ! (imgageTypes & ct.toSet).isEmpty
   
   def fromAcceptedContentTypes(ct: Iterable[String]): Representation = {
     Lang(ct) map RDFRepr.apply getOrElse {
+      Image(ct).map(ImageRepr.apply(_)).getOrElse {
       if (acceptsHTML(ct))
         HTMLRepr
       else
         UnknownRepr
+      }
     }
   }
   
@@ -52,6 +61,7 @@
 }
 
 case class RDFRepr(lang: Lang) extends Representation
+case class ImageRepr(mime: Image) extends Representation
 case object HTMLRepr extends Representation
 case object DirectoryRepr extends Representation
 case object UnknownRepr extends Representation
--- a/src/main/scala/Resource.scala	Wed Apr 11 14:56:25 2012 +0200
+++ b/src/main/scala/Resource.scala	Fri Apr 13 17:43:15 2012 +0200
@@ -6,6 +6,7 @@
 import com.hp.hpl.jena.rdf.model._
 import scalaz.{Resource => _, _}
 import Scalaz._
+import java.io.InputStream
 
 trait ResourceManager {
   def basePath:String
@@ -19,12 +20,41 @@
 
 trait Resource {
   def name: URL
+
+  /**
+   * get the resource as a model.
+   * Note: the cache policy only really makes sense for remote resources, not for the file system.
+   * Note: returning a model only makes sense when the resource is something that can be transformed to
+   *       one which is not always the case, especially not for images. (see below)
+   * @param policy
+   * @return
+   */
   def get(policy: CacheControl.Value = CacheControl.CacheFirst): Validation[Throwable, Model]
+
+  /**
+   * Many resources are not images. We need to get inputStreams for this.
+   * (this model here is getting more and more complicated. The get that returns a model above cannot
+   * simply be reduced to this one, as in the FileResource doing a GET on the directory should return
+   * a Graph describing the directory for example. Dealing with this is going to be a bit more tricky than
+   * I (bblfish) have time for at this point - as it would probably require quite a deep rewrite.)
+   * )
+   * @return
+   */
+  def getStream: Validation[Throwable,InputStream]
   def delete: Validation[Throwable, Unit]
   def save(model:Model):Validation[Throwable, Unit]
 
+  /**
+   * PUT the inputstream in the location
+   *
+   * same comments as with getStream
+   *
+   * @param in inputstream containing serialisation
+   */
+  def putStream(in: InputStream): Validation[Throwable, Unit]
+
   //These two methods only work when called on directories
-  def createDirectory(model: Model): Validation[Throwable, Unit]
+  def createDirectory: Validation[Throwable, Unit]
   def create(): Validation[Throwable, Resource]
 }
 
--- a/src/main/scala/auth/WebIdClaim.scala	Wed Apr 11 14:56:25 2012 +0200
+++ b/src/main/scala/auth/WebIdClaim.scala	Fri Apr 13 17:43:15 2012 +0200
@@ -87,7 +87,7 @@
               if (mod.getDatatype == XSDDatatype.XSDhexBinary &&
                 new BigInteger(stripSpace(mod.getLexicalForm), 16) == rsakey.getModulus) {
                 val exp = sol.getLiteral("e")
-                numericDataTypes.contains(exp.getDatatype) && new BigInteger(exp.getLexicalForm) == rsakey.getPublicExponent
+                numericDataTypes.contains(exp.getDatatype) && new BigInteger(exp.getLexicalForm.trim) == rsakey.getPublicExponent
               } else false
             } catch {
               case _ => false
--- a/src/main/scala/netty/ReadWriteWebNetty.scala	Wed Apr 11 14:56:25 2012 +0200
+++ b/src/main/scala/netty/ReadWriteWebNetty.scala	Fri Apr 13 17:43:15 2012 +0200
@@ -142,11 +142,9 @@
 
 case class ResponseBin(bis: InputStream) extends ResponseStreamer {
   override def stream(out: OutputStream) {
-    var c=0
-    val buf = new Array[Byte](1024)
-    do {
-      c = bis.read(buf)
-      if (c > 0) out.write(buf,0,c)
-    } while (c > -1)
+    val buf = new Array[Byte](4096)
+    Iterator continually bis.read(buf) takeWhile (-1 !=) foreach  { bytesRead =>
+      out.write(buf,0,bytesRead)
+    }
   }
 }
\ No newline at end of file