Added initial WebID code. WebIDs are verified, but verification is not cashed so it is slow. Authentification is not used yet either.
--- a/.hgignore	Thu Oct 06 20:27:12 2011 -0400
+++ b/.hgignore	Wed Oct 12 00:21:48 2011 +0200
@@ -15,4 +15,6 @@
 src/test/scala.egp
 sbt-launch*.jar
 .scala_dependencies
-*.orig
\ No newline at end of file
+*.orig
+.idea
+.idea_modules
--- a/README.markdown	Thu Oct 06 20:27:12 2011 -0400
+++ b/README.markdown	Wed Oct 12 00:21:48 2011 +0200
@@ -73,3 +73,14 @@
  *   --strict  Documents must be created using PUT else they return 404
     
     
+HTTPS with WebID 
+----------------
+
+### to run on https with WebID
+
+    > java -Djetty.ssl.keyStoreType=JKS -Djetty.ssl.keyStore=KEYSTORE.jks -Djetty.ssl.keyStorePassword=secret -jar target/read-write-web.jar -https 8443
+
+### with debug enabled  add the following parameters after 'java'
+
+     -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
+
--- a/project/build.scala	Thu Oct 06 20:27:12 2011 -0400
+++ b/project/build.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -5,13 +5,13 @@
 // they are pulled only if used
 object Dependencies {
   val specs = "org.scala-tools.testing" %% "specs" % "1.6.9" % "test"
-  val dispatch = "net.databinder" %% "dispatch-http" % "0.8.5" % "test"
-  val unfiltered_filter = "net.databinder" %% "unfiltered-filter" % "0.4.1"
-  val unfiltered_jetty = "net.databinder" %% "unfiltered-jetty" % "0.4.1"
+  val dispatch_http = "net.databinder" %% "dispatch-http" % "0.8.5" 
+  val unfiltered_filter = "net.databinder" %% "unfiltered-filter" % "0.5.0"
+  val unfiltered_jetty = "net.databinder" %% "unfiltered-jetty" % "0.5.0"
   // val unfiltered_spec = "net.databinder" %% "unfiltered-spec" % "0.4.1" % "test"
   val ivyUnfilteredSpec =
     <dependencies>
-      <dependency org="net.databinder" name="unfiltered-spec_2.9.1" rev="0.4.1">
+      <dependency org="net.databinder" name="unfiltered-spec_2.9.1" rev="0.5.0">
         <exclude org="net.databinder" module="dispatch-mime_2.9.0-1"/>
       </dependency>
     </dependencies>
@@ -21,8 +21,7 @@
   val arq = "com.hp.hpl.jena" % "arq" % "2.8.8"
   val grizzled = "org.clapper" %% "grizzled-scala" % "1.0.8" % "test"
   val scalaz = "org.scalaz" %% "scalaz-core" % "6.0.2"
-
-
+  val jsslutils = "org.jsslutils" % "jsslutils" % "1.0.7"
 
 }
 
@@ -78,7 +77,7 @@
       libraryDependencies += specs,
 //      libraryDependencies += unfiltered_spec,
       ivyXML := ivyUnfilteredSpec,
-      libraryDependencies += dispatch,
+      libraryDependencies += dispatch_http,
       libraryDependencies += unfiltered_filter,
       libraryDependencies += unfiltered_jetty,
 //      libraryDependencies += slf4jSimple,
@@ -87,6 +86,8 @@
       libraryDependencies += antiXML,
       libraryDependencies += grizzled,
       libraryDependencies += scalaz,
+      libraryDependencies += jsslutils,
+
       jarName in assembly := "read-write-web.jar"
     )
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/AuthFilter.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -0,0 +1,379 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms are permitted
+ * provided that the above copyright notice and this paragraph are
+ * duplicated in all such forms and that any documentation,
+ * advertising materials, and other materials related to such
+ * distribution and use acknowledge that the software was developed
+ * by Henry Story.  The name of bblfish.net may not be used to endorse
+ * or promote products derived
+ * from this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+package org.w3.readwriteweb.webid
+
+
+
+import java.security.cert.X509Certificate
+import javax.servlet._
+import org.slf4j.LoggerFactory
+import org.w3.readwriteweb._
+
+import java.util.{LinkedList, Date}
+import java.security.interfaces.RSAPublicKey
+import java.security.{Principal, PublicKey}
+import java.net.URL
+import java.math.BigInteger
+import com.hp.hpl.jena.rdf.model.RDFNode
+import collection.JavaConversions._
+import javax.security.auth.{Subject, Refreshable}
+import com.hp.hpl.jena.query._
+
+/**
+ * @author Henry Story from http://bblfish.net/
+ * @created: 09/10/2011
+ */
+
+case class WebIdPrincipal(webid: String) extends Principal {
+  def getName = webid
+  override def equals(that: Any) = that match {
+    case other: WebIdPrincipal => other.webid == webid
+    case _ => false
+  }
+}
+
+case class Anonymous() extends Principal {
+  def getName = "anonymous"
+  override def equals(that: Any) =  that match {
+      case other: WebIdPrincipal => other eq this 
+      case _ => false
+    } //anonymous principals are equal only when they are identical. is this wise?
+      //well we don't know when two anonymous people are the same or different.
+}
+
+class AuthFilter extends Filter {
+  def init(filterConfig: FilterConfig) {}
+
+  def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
+    val certChain = request.getAttribute("javax.servlet.request.X509Certificate") match {
+      case certs: Array[X509Certificate] => certs.toList
+      case _ => Nil
+    }
+    val subject = new Subject()
+    if (certChain.size == 0) {
+      System.err.println("No certificate found!")
+      subject.getPrincipals.add(Anonymous())
+    } else {
+      val x509c = new X509Claim(certChain(0))
+      subject.getPublicCredentials.add(x509c)
+
+
+      val verified = for (
+        claim <- x509c.webidclaims;
+        if (claim.verified)
+      ) yield claim.principal
+
+      subject.getPrincipals.addAll(verified)
+      System.err.println("Found "+verified.size+" principals: "+verified)
+    }
+
+    chain.doFilter(request, response)
+  }
+
+  def destroy() {}
+}
+
+object X509Claim {
+  final val logger = LoggerFactory.getLogger(classOf[X509Claim])
+
+  /**
+   * Extracts the URIs in the subject alternative name extension of an X.509
+   * certificate
+   *
+   * @param cert X.509 certificate from which to extract the URIs.
+   * @return Iterator of URIs as strings found in the subjectAltName extension.
+   */
+	def getClaimedWebIds(cert: X509Certificate): Iterator[String] =
+    if (cert == null)  Iterator.empty;
+    else cert.getSubjectAlternativeNames() match {
+      case coll if (coll != null) => {
+        for (sanPair <- coll
+             if (sanPair.get(0) == 6)
+        ) yield sanPair(1).asInstanceOf[String]
+      }.iterator
+      case _ => Iterator.empty
+    }
+
+
+
+}
+
+
+/**
+ * An X509 Claim maintains information about the proofs associated with claims
+ * found in an X509 Certificate. It is the type of object that can be passed
+ * into the public credentials part of a Subject node
+ *
+ * todo: think of what this would look like for a chain of certificates
+ *
+ * @author bblfish
+ * @created: 30/03/2011
+ */
+class X509Claim(val cert: X509Certificate) extends Refreshable {
+
+  import X509Claim._
+  val claimReceivedDate = new Date();
+  lazy val tooLate = claimReceivedDate.after(cert.getNotAfter())
+  lazy val tooEarly = claimReceivedDate.before(cert.getNotBefore())
+
+  /* a list of unverified principals */
+  lazy val webidclaims = getClaimedWebIds(cert).map {
+    webid =>new WebIDClaim(webid, cert.getPublicKey)
+  }.toSet
+
+
+  //note could also implement Destroyable
+  //
+  //http://download.oracle.com/javase/6/docs/technotes/guides/security/jaas/JAASRefGuide.html#Credentials
+  //
+  //if updating validity periods can also take into account the WebID reference, then it is possible
+  //that a refresh could have as consequence to do a fetch on the WebID profile
+  //note: one could also take the validity period to be dependent on the validity of the profile representation
+  //in which case updating the validity period would make more sense.
+
+  override
+  def refresh() {
+  }
+
+  /* The certificate is currently within the valid time zone */
+  override
+  def isCurrent(): Boolean = !(tooLate||tooEarly)
+
+  lazy val error = {}
+
+  def canEqual(other: Any) = other.isInstanceOf[X509Claim]
+
+  override
+  def equals(other: Any): Boolean =
+    other match {
+      case that: X509Claim => (that eq this) || (that.canEqual(this) && cert == that.cert)
+      case _ => false
+    }
+
+  override
+  lazy val hashCode: Int = 41 * (41 +
+    (if (cert != null) cert.hashCode else 0))
+
+
+}
+
+object WebIDClaim {
+     final val cert: String = "http://www.w3.org/ns/auth/cert#"
+     final val xsd: String = "http://www.w3.org/2001/XMLSchema#"
+
+    val selectQuery = QueryFactory.create("""
+ 		  PREFIX cert: <http://www.w3.org/ns/auth/cert#>
+ 		  PREFIX rsa: <http://www.w3.org/ns/auth/rsa#>
+ 		  SELECT ?key ?m ?e ?mod ?exp
+ 		  WHERE {
+ 		   ?key cert:identity ?webid ;
+ 		      rsa:modulus ?m ;
+ 		      rsa:public_exponent ?e .
+ 
+ 		    OPTIONAL { ?m cert:hex ?mod . }
+ 		    OPTIONAL { ?e cert:decimal ?exp . }
+ 		  }""")
+
+  /**
+    * Transform an RDF representation of a number into a BigInteger
+    * <p/>
+    * Passes a statement as two bindings and the relation between them. The
+    * subject is the number. If num is already a literal number, that is
+    * returned, otherwise if enough information from the relation to optstr
+    * exists, that is used.
+    *
+    * @param num the number node
+    * @param optRel name of the relation to the literal
+    * @param optstr the literal representation if it exists
+    * @return the big integer that num represents, or null if undetermined
+    */
+   def toInteger(num: RDFNode, optRel: String, optstr: RDFNode): Option[BigInteger] =
+       if (null == num) None
+       else if (num.isLiteral) {
+         val lit = num.asLiteral()
+         toInteger_helper(lit.getLexicalForm,lit.getDatatypeURI)
+       } else if (null != optstr && optstr.isLiteral) {
+           toInteger_helper(optstr.asLiteral().getLexicalForm,optRel)
+       } else None
+
+
+    private def intValueOfHexString(s: String): BigInteger = {
+      val strval = cleanHex(s);
+      new BigInteger(strval, 16);
+    }
+
+
+    /**
+     * This takes any string and returns in order only those characters that are
+     * part of a hex string
+     *
+     * @param strval
+     *            any string
+     * @return a pure hex string
+     */
+
+    private def cleanHex(strval: String) = {
+      def legal(c: Char) = {
+        //in order of appearance probability
+        ((c >= '0') && (c <= '9')) ||
+          ((c >= 'A') && (c <= 'F')) ||
+          ((c >= 'a') && (c <= 'f'))
+      }
+      (for (c <- strval; if legal(c)) yield c)
+    }
+
+
+   /**
+    * This transforms a literal into a number if possible ie, it returns the
+    * BigInteger of "ddd"^^type
+    *
+    * @param num the string representation of the number
+    * @param tpe the type of the string representation
+    * @return the number
+    */
+   protected def toInteger_helper(num: String, tpe: String): Option[BigInteger] =
+     try {
+       if (tpe.equals(cert + "decimal") || tpe.equals(cert + "int")
+         || tpe.equals(xsd + "integer") || tpe.equals(xsd + "int")
+         || tpe.equals(xsd + "nonNegativeInteger")) {
+         // cert:decimal is deprecated
+         Some(new BigInteger(num.trim(), 10));
+       } else if (tpe.equals(cert + "hex")) {
+         Some(intValueOfHexString(num));
+       } else {
+         // it could be some other encoding - one should really write a
+         // special literal transformation class
+         None;
+       }
+     } catch {
+       case e: NumberFormatException => None
+     }
+
+
+}
+
+/**
+ * An X509 Claim maintains information about the proofs associated with claims
+ * found in an X509 Certificate. It is the type of object that can be passed
+ * into the public credentials part of a Subject node
+ *
+ * todo: think of what this would look like for a chain of certificates
+ *
+ * @author bblfish
+ * @created 30/03/2011
+ */
+class WebIDClaim(val webId: String, val key: PublicKey) {
+
+	val errors = new LinkedList[java.lang.Throwable]()
+
+	lazy val principal = new WebIdPrincipal(webId)
+	lazy val tests: List[Verification] = verify()  //I need to try to keep more verification state
+
+
+	/**
+	 * verify this claim
+	 * @param authSrvc: the authentication service contains information about where to get graphs
+	 */
+	//todo: make this asynchronous
+	lazy val verified: Boolean =  tests.exists(v => v.isInstanceOf[Verified])
+
+  private def verify(): List[Verification] = {
+    import util.wrapValidation
+    import collection.JavaConversions._
+    import WebIDClaim._
+    if (!webId.startsWith("http:") && !webId.startsWith("https:")) {
+      //todo: ftp, and ftps should also be doable, though content negotiations is then lacking
+      unsupportedProtocol::Nil
+    } else if (!key.isInstanceOf[RSAPublicKey]) {
+      certificateKeyTypeNotSupported::Nil
+    } else {
+      val cache = Lookup.get(classOf[WebCache]).head
+      val res = for {
+        model <- cache.resource(new URL(webId)).get() failMap {
+          t => new ProfileError("error fetching profile", t)
+        }
+      } yield {
+        val initialBinding = new QuerySolutionMap();
+        initialBinding.add("webid",model.createResource(webId))
+        val qe: QueryExecution = QueryExecutionFactory.create(WebIDClaim.selectQuery, model,initialBinding)
+        try {
+          qe.execSelect().map( qs => {
+            val modulus = toInteger(qs.get("m"), cert + "hex", qs.get("mod"))
+            val exponent = toInteger(qs.get("e"), cert + "decimal", qs.get("exp"))
+
+            (modulus, exponent) match {
+              case (Some(mod), Some(exp)) => {
+                val rsakey = key.asInstanceOf[RSAPublicKey]
+                if (rsakey.getPublicExponent == exp && rsakey.getModulus == mod) verifiedWebID
+                else keyDoesNotMatch
+              }
+              case _ => new KeyProblem("profile contains key that cannot be analysed:" +
+                qs.varNames().map(nm => nm + "=" + qs.get(nm).toString) + "; ")
+            }
+          }).toList
+          //it would be nice if we could keep a lot more state of what was verified and how
+          //will do that when implementing tests, so that these tests can then be used directly as much as possible
+        } finally {
+          qe.close()
+        }
+      }
+      res.either match {
+        case Right(tests) => tests
+        case Left(profileErr) => profileErr::Nil
+      }
+    }
+
+
+  }
+  
+
+
+	def canEqual(other: Any) = other.isInstanceOf[WebIDClaim]
+
+	override
+	def equals(other: Any): Boolean =
+		other match {
+			case that: WebIDClaim => (that eq this) || (that.canEqual(this) && webId == that.webId && key == that.key)
+			case _ => false
+		}
+
+	override
+	lazy val hashCode: Int = 41 * (
+		41 * (
+			41 + (if (webId != null) webId.hashCode else 0)
+			) + (if (key != null) key.hashCode else 0)
+		)
+}
+
+
+class Verification(msg: String)
+class Verified(msg: String) extends Verification(msg)
+class Unverified(msg: String) extends Verification(msg)
+
+class TestFailure(msg: String) extends Verification(msg)
+class ProfileError(msg: String,  t: Throwable ) extends TestFailure(msg)
+class KeyProblem(msg: String) extends TestFailure(msg)
+
+object unsupportedProtocol extends TestFailure("WebID protocol not supported")
+object noMatchingKey extends TestFailure("No keys in profile matches key in cert")
+object keyDoesNotMatch extends TestFailure("Key does not match")
+
+object verifiedWebID extends Verified("WebId verified")
+object notstarted extends Unverified("No verification attempt started")
+object failed extends Unverified("Tests failed")
+object certificateKeyTypeNotSupported extends TestFailure("The certificate key type is not supported. We only support RSA")
\ No newline at end of file
--- a/src/main/scala/Main.scala	Thu Oct 06 20:27:12 2011 -0400
+++ b/src/main/scala/Main.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -16,6 +16,7 @@
 import com.hp.hpl.jena.query._
 import com.hp.hpl.jena.update._
 import com.hp.hpl.jena.shared.JenaException
+import org.w3.readwriteweb.{Resource}
 import Query.{QueryTypeSelect => SELECT, QueryTypeAsk => ASK,
               QueryTypeConstruct => CONSTRUCT, QueryTypeDescribe => DESCRIBE}
 
@@ -23,6 +24,15 @@
 import Scalaz._
 
 import org.w3.readwriteweb.util._
+import java.security.KeyStore
+import org.jsslutils.keystores.KeyStoreLoader
+import org.jsslutils.sslcontext.{X509TrustManagerWrapper, X509SSLContextFactory}
+import javax.net.ssl.{X509TrustManager, SSLContext}
+import org.jsslutils.sslcontext.trustmanagers.TrustAllClientsWrappingTrustManager
+import java.security.cert.X509Certificate
+import scala.sys.SystemProperties
+import collection.{mutable,immutable}
+import webid.AuthFilter
 
 class ReadWriteWeb(rm:ResourceManager) {
   
@@ -137,51 +147,132 @@
 
 }
 
+object Lookup {
+  // a place to register services that can be looked up from anywhere.
+  // this is very naive registration, compared to tools like Clerezza that use Apaches Felix's OSGI implementation
+
+  private val db = new mutable.HashMap[Class[_],AnyRef]
+
+  def get[T<:AnyRef](clzz :Class[T]) :Option[T] = db.get(clzz).map(e=>e.asInstanceOf[T])
+
+  //http://stackoverflow.com/questions/3587286/how-does-scalas-2-8-manifest-work
+  def put[T<:AnyRef : Manifest](obj: T): T = {
+    def zref = manifest[T].erasure
+    val ref: AnyRef = obj
+    db.put(zref,ref).asInstanceOf[T]
+  }
+
+}
 
 object ReadWriteWebMain {
 
   val logger:Logger = LoggerFactory.getLogger(this.getClass)
 
+
   // regular Java main
   def main(args: Array[String]) {
-    
+   
     val argsList = args.toList
+    var httpPort: Int = 8080
+    var httpsPort: Option[Int] = None
+    var baseDir = new File(".")
+    var baseUrl: String = "/"
+    var relax = false
     
-    val (port, baseDirectory, baseURL) = argsList match {
-      case port :: directory :: base :: _ => (port.toInt, new File(directory), base)
-      case _ => {
-        println(
-"""example usage:
-    java -jar read-write-web.jar 8080 ~/WWW/2011/09 /2011/09 [-strict|-relax]
+ 
+     def msg(err: String, exitCode: Int=1) = {
+        println("ERROR:")
+        println(err)
+        println("""
+
+example usage:
+java -jar read-write-web.jar [-http 8080] [-https 8443] -dir ~/WWW/2011/09 -base /2011/09 [-strict|-relax]
+
+Required:
+  -dir $localpath :  the directory where the files are located
+  -base $urlpath : the base url-path for those files
 
 Options:
+ -http $port  : set the http port to the port number, by default this will be port 8080
+ -https $port : start the https server on the given port number
  -relax all resources potentially exist, meaning you get an empty RDF graph instead of a 404 (still experimental)
  -strict a GET on a resource will fail with a 404 (default mode if you omit it)
-""")
-        System.exit(1)
+
+Properties:  (can be passed with -Dprop=value)
+
+ * Keystore properties.
+  jetty.ssl.keyStoreType : the type of the keystore, JKS by default usually
+  jetty.ssl.keyStore=path : specify path to key store (for https server certificate)
+  jetty.ssl.keyStorePassword=password : specify password for keystore store
+
+ * Trust store
+   Trust stores are not needed because we use the WebID protocol, and client certs are nearly never signed by CAs
+ """)
+        System.exit(exitCode)
         null
+    }
+    
+
+    def parse(args: List[String]): Unit = {
+      val res = args match {
+        case "-https"::num::rest =>  { 
+          httpsPort = Some(Integer.parseInt(num))
+          rest
+        }
+        case "-http"::num::rest => {
+          httpPort = num.toInt
+          rest
+        }
+        case "-dir"::dir::rest => {
+          baseDir = new File(dir)
+          rest
+        }
+        case "-base"::path::rest=> {
+          baseUrl=path
+          rest
+        }
+        case "-strict"::rest=> {
+          relax = false
+          rest
+        }
+        case "-relax"::rest=> {
+          relax = true
+          rest
+        }
+        case something::other => msg("could not parse command: `"+something+"`",1)
+        case Nil => return
       }
+      parse(res)
     }
+    
+    parse(argsList)
 
     val mode =
-      if (argsList contains "-relax") {
+      if (relax) {
         logger.info("info: using experimental relax mode")
         AllResourcesAlreadyExist
       } else {
         ResourcesDontExistByDefault
       }
     
-    if (! baseDirectory.exists) {
-      println("%s does not exist" format (baseDirectory.getAbsolutePath))
-      System.exit(2)
+    if (! baseDir.exists) {
+      msg("%s does not exist" format (baseDir.getAbsolutePath),2)
     }
 
-    val filesystem = new Filesystem(baseDirectory, baseURL, lang="TURTLE")(mode)
+    val filesystem = new Filesystem(baseDir, baseUrl, lang="TURTLE")(mode)
     
     val app = new ReadWriteWeb(filesystem)
 
+    val service = httpsPort match {
+      case Some(port) => HttpsTrustAll(port,"0.0.0.0")
+      case None => Http(httpPort)
+    }
+
+    val webCache = new WebCache()
+    Lookup.put(webCache)
+
     // configures and launches a Jetty server
-    unfiltered.jetty.Http(port).filter {
+    service.filter {
       // a jee Servlet filter that logs HTTP requests
       new Filter {
         def destroy():Unit = ()
@@ -195,7 +286,8 @@
         def init(filterConfig:FilterConfig):Unit = ()
       }
     // Unfiltered filters
-    }.context("/public"){ ctx:ContextBuilder =>
+    }.filter(new AuthFilter)
+     .context("/public"){ ctx:ContextBuilder =>
       ctx.resources(MyResourceManager.fromClasspath("public/").toURI.toURL)
     }.filter(app.read).run()
     
@@ -203,3 +295,38 @@
 
 }
 
+case class HttpsTrustAll(override val port: Int, override val host: String) extends Https(port, host) with TrustAll
+
+trait TrustAll { self: Ssl =>
+   import scala.sys.SystemProperties._
+
+   lazy val sslContextFactory = new X509SSLContextFactory(
+               serverCertKeyStore,
+               tryProperty("jetty.ssl.keyStorePassword"),
+               serverCertKeyStore); //this one is not needed since our wrapper ignores all trust managers
+
+   lazy val trustWrapper = new X509TrustManagerWrapper {
+     def wrapTrustManager(trustManager: X509TrustManager) = new TrustAllClientsWrappingTrustManager(trustManager)
+   }
+
+   lazy val serverCertKeyStore = {
+      val keyStoreLoader = new KeyStoreLoader
+   		keyStoreLoader.setKeyStoreType(System.getProperty("jetty.ssl.keyStoreType","JKS"))
+   		keyStoreLoader.setKeyStorePath(trustStorePath)
+   		keyStoreLoader.setKeyStorePassword(System.getProperty("jetty.ssl.keyStorePassword","password"))
+      keyStoreLoader.loadKeyStore();
+   }
+
+   sslContextFactory.setTrustManagerWrapper(trustWrapper);
+
+
+ 	 lazy val trustStorePath =  new SystemProperties().get("jetty.ssl.keyStore") match {
+       case Some(path) => path
+       case None => new File(new File(tryProperty("user.home")), ".keystore").getAbsolutePath
+   }
+
+   sslConn.setSslContext(sslContextFactory.buildSSLContext())
+   sslConn.setWantClientAuth(true)
+
+}
+
--- a/src/main/scala/Resource.scala	Thu Oct 06 20:27:12 2011 -0400
+++ b/src/main/scala/Resource.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -8,8 +8,6 @@
 import com.hp.hpl.jena.rdf.model._
 import com.hp.hpl.jena.shared.JenaException
 
-import org.w3.readwriteweb.util._
-
 import scalaz._
 import Scalaz._
 
@@ -20,6 +18,7 @@
   def sanityCheck():Boolean
   def resource(url:URL):Resource
 }
+
 trait Resource {
   def get():Validation[Throwable, Model]
   def save(model:Model):Validation[Throwable, Unit]
@@ -76,3 +75,4 @@
   }
   
 }
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/WebCache.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -0,0 +1,75 @@
+ /*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms are permitted
+ * provided that the above copyright notice and this paragraph are
+ * duplicated in all such forms and that any documentation,
+ * advertising materials, and other materials related to such
+ * distribution and use acknowledge that the software was developed
+ * by Henry Story.  The name of bblfish.net may not be used to endorse 
+ * or promote products derived
+ * from this software without specific prior written permission.
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+ */ 
+
+package org.w3.readwriteweb
+
+import com.hp.hpl.jena.rdf.model.Model
+import java.net.URL
+import org.apache.http.MethodNotSupportedException
+import org.w3.readwriteweb.RDFEncoding._
+import org.w3.readwriteweb.util._
+import org.w3.readwriteweb.{RDFEncoding, RDFXML}
+import scalaz._
+import Scalaz._
+
+/**
+ * @author Henry Story
+ * @created: 12/10/2011
+ *
+ * The WebCache currently does not cache
+ */
+class WebCache extends ResourceManager {
+  import dispatch._
+
+  val http = new Http
+  
+  def basePath = null //should be cache dir?
+
+  def sanityCheck() = true  //cache dire exists? But is this needed for functioning?
+
+  def resource(u : URL) = new org.w3.readwriteweb.Resource {
+
+    def get() = {
+      // note we prefer rdf/xml and turtle over html, as html does not always contain rdfa, and we prefer those over n3,
+      // as we don't have a full n3 parser. Better would be to have a list of available parsers for whatever rdf framework is
+      // installed (some claim to do n3 when they only really do turtle)
+      // we can't currently accept */* as we don't have GRDDL implemented
+      val request = url(u.toString) <:< Map("Accept"->
+        "application/rdf+xml,text/turtle,application/xhtml+xml;q=0.8,text/html;q=0.7,text/n3;q=0.6")
+
+      //we need to tell the model about the content type
+      val handler: Handler[Validation[Throwable, Model]] = request.>+>[Validation[Throwable, Model]](res =>  {
+        res >:> { headers =>
+          val encoding = headers("Content-Type").headOption match {
+            case Some(mime) => RDFEncoding(mime)
+            case None => RDFXML  // it would be better to try to do a bit of guessing in this case by looking at content
+          }
+          val loc = headers("Content-Location").headOption match {
+            case Some(loc) => new URL(u,loc)
+            case None => new URL(u.getProtocol,u.getAuthority,u.getPort,u.getPath)
+          }
+          res>>{ in=>modelFromInputStream(in,loc.toString,encoding) }
+
+        }
+      })
+      http(handler)
+
+    }
+
+    def save(model: Model) = { throw new MethodNotSupportedException("not implemented"); null }
+  }
+}
--- a/src/main/scala/util.scala	Thu Oct 06 20:27:12 2011 -0400
+++ b/src/main/scala/util.scala	Wed Oct 12 00:21:48 2011 +0200
@@ -23,7 +23,6 @@
 import unfiltered.request._
 import unfiltered.response._
 import unfiltered.jetty._
-
 sealed trait RWWMode
 case object AllResourcesAlreadyExist extends RWWMode
 case object ResourcesDontExistByDefault extends RWWMode
@@ -40,12 +39,21 @@
 
 object RDFEncoding {
   
-  def apply(contentType:String):RDFEncoding =
-    contentType match {
+  def apply(contentType:String):RDFEncoding = {
+    val i = contentType.indexOf(';')
+    (if (i<0) contentType
+    else contentType.substring(0,i).trim()).toLowerCase match {
       case "text/turtle" => TURTLE
       case "application/rdf+xml" => RDFXML
-      case _ => RDFXML
+      case _ => RDFXML       
     }
+  }
+
+  def jena(encoding: RDFEncoding) = encoding match {
+    case RDFXML => "RDF/XML-ABBREV"
+    case TURTLE => "TURTLE"
+    case _      => "RDF/XML-ABBREV" //don't like this default
+  }
     
   def apply(req:HttpRequest[_]):RDFEncoding = {
     val contentType = Accept(req).headOption
@@ -60,7 +68,7 @@
 }
 
 package object util {
-  
+
   val defaultLang = "RDF/XML-ABBREV"
 
   class MSAuthorVia(value:String) extends ResponseHeader("MS-Author-Via", List(value))
@@ -88,6 +96,9 @@
       }
   }
 
+  def modelFromInputStream(is:InputStream, base: String,  lang: RDFEncoding): Validation[Throwable, Model]=
+    modelFromInputStream(is, base, RDFEncoding.jena(lang))
+  
   def modelFromInputStream(
       is:InputStream,
       base:String,