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,