diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ef5f69b32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swp +node_modules/ diff --git a/Makefile b/Makefile index 1cbfb9959..bc93fd357 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ run-for-test: NODE_PATH=.:../tabulator-firefox/content/js/rdf/dist/ node server.js \ - -v --uriBase http://localhost:3000/test/ --fileBase `pwd`/test/ + -v --uriBase http://localhost:3000/test/ -p 3000 --fileBase `pwd`/test/ diff --git a/acl.js b/acl.js new file mode 100644 index 000000000..7b0de4f3e --- /dev/null +++ b/acl.js @@ -0,0 +1,78 @@ +var acl = require('acl'); +var fs = require('fs'); +var path = require('path'); + +var file = require('./fileStore.js'); +var options = require('./options.js'); + +var permissionExtension = '.permissions'; +var aclEnabled = false; + +module.exports.readPermissions = function() { + var permissionFile = options.fileBase + permissionExtension; + fs.readFile(permissionFile, readPermissionsCallback); + + function readPermissionsCallback(err, rawPermissions) { + if (err) { + return; + } else { + var jsonPermissions; + try { + jsonPermissions = JSON.parse(rawPermissions); + } catch (parseErr) { + return; + } + if ('roles' in jsonPermissions && 'users' in jsonPermissions) { + try { + acl = new acl(new acl.memoryBackend()); + for (var rIndex = 0; rIndex < jsonPermissions.roles.length; rIndex++) { + acl.allow(jsonPermissions.roles[rIndex][0], + jsonPermissions.roles[rIndex][1], + jsonPermissions.roles[rIndex][2]); + } + for (var uIndex = 0; uIndex < jsonPermissions.users.length; uIndex++) { + acl.addUserRoles(jsonPermissions.users[uIndex][0], + jsonPermissions.users[uIndex][1]); + } + aclEnabled = true; + return; + } catch (permissionErr) { + return; + } + } else { + return; + } + } + } +}; + +module.exports.isAllowed = function(userId, resource, permissions, callback) { + if (aclEnabled) { + acl.isAllowed(userId, resource, permissions, callback); + } else { + callback(undefined, true); + } +}; + +module.exports.aclHandler = function(req, res, next) { + //TODO authentication + var filename = file.uriToFilename(req.path); + var resource = path.relative(options.fileBase, filename); + + //TODO complete this line + // acl.isAllowed(userId, resource, req.method); + next(); + + function aclCallback(err, allowed) { + if (err) { + return res.sendStatus(500); + } else { + if (allowed) { + next(); + } else { + return res.sendStatus(403); + } + } + + } +}; diff --git a/container.js b/container.js new file mode 100644 index 000000000..7ec6a04a3 --- /dev/null +++ b/container.js @@ -0,0 +1,228 @@ +var fs = require('fs'); +var $rdf = require('rdflib'); +var path = require('path'); +var uuid = require('node-uuid'); + +var logging = require('./logging.js'); +var metadata = require('./metadata.js'); +var options = require('./options.js'); + +var rdfVocab = require('./vocab/rdf.js'); +var ldpVocab = require('./vocab/ldp.js'); + +var addUriTriple = function(kb, s, o, p) { + kb.add(kb.sym(s), kb.sym(o), kb.sym(p)); +}; + +var usedURIs = {}; + +module.exports.createRootContainer = function() { + if (!metadata.hasMetadata(options.fileBase)) { + logging.log("Creating root metadata"); + var rootMetadata = new metadata.Metadata(); + rootMetadata.filename = options.fileBase; + rootMetadata.isResource = true; + rootMetadata.isContainer = true; + rootMetadata.isSourceResource = true; + rootMetadata.isBasicContainer = true; + metadata.writeMetadata(options.fileBase, rootMetadata, + writeCallback); + } + //TODO handle case when .container file does not exist + + function writeCallback(err) { + logging.log(options.pathStart); + if (err) { + process.exit(1); + } else if (!metadata.hasContainerMetadata(options.fileBase)) { + var rootContainer = $rdf.graph(); + addUriTriple(rootContainer, options.pathStart, rdfVocab.type, + ldpVocab.Resource); + addUriTriple(rootContainer, options.pathStart, rdfVocab.type, + ldpVocab.RDFSource); + addUriTriple(rootContainer, options.pathStart, rdfVocab.type, + ldpVocab.Container); + addUriTriple(rootContainer, options.pathStart, rdfVocab.type, + ldpVocab.BasicContainer); + rootContainer.add(rootContainer.sym(options.pathStart), + rootContainer.sym('http://purl.org/dc/terms/title'), + '"Root Container"'); + var serializedContainer = $rdf.serialize(undefined, rootContainer, + options.pathStart, 'text/turtle'); + logging.log("Root container: ", serializedContainer); + metadata.writeContainerMetadata(options.fileBase, + serializedContainer, function(err) { + if (err) { + //TODO handle error + logging.log("Could not write root container"); + } else { + logging.log("Wrote root container"); + } + }); + } + } +}; + +module.exports.createNewContainer = function(container, type, callback) { + fs.mkdir(container, function(err) { + if (err) { + this.releaseResourceUri(container); + callback(err); + } else { + var containerMetadata = new metadata.Metadata(); + containerMetadata.filename = container; + containerMetadata.isResource = true; + containerMetadata.isContainer = true; + containerMetadata.isSourceResource = true; + if (type === ldpVocab.BasicContainer) + containerMetadata.isBasicContainer = true; + if (type === ldpVocab.DirectContainer) + containerMetadata.isDirectContainer = true; + metadata.writeMetadata(options.fileBase, rootMetadata, writeCallback); + } + }); + + function writeCallback(err) { + if (err) { + this.releaseResourceUri(container); + callback(err); + } else { + var newContainer = $rdf.graph(); + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.Resource); + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.RDFSource); + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.Container); + if (type === ldpVocab.BasicContainer) + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.BasicContainer); + else if (type === ldpVocab.DirectContainer) + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.DirectContainer); + + newContainer.add(newContainer.sym(options.pathStart), + newContainer.sym('http://purl.org/dc/terms/title'), + container); + var serializedContainer = $rdf.serialize(undefined, newContainer, + container, 'text/turtle'); + metadata.writeContainerMetadata(container, serializedContainer, + function(err) { + this.releaseResourceUri(container); + if (err) { + callback(err); + } else { + logging.log("Wrote new container"); + callback(err); + } + }); + } + } + +}; + +module.exports.createResourceUri = function(containerURI, slug) { + var newPath; + if (slug) { + newPath = path.join(containerURI, slug); + } else { + newPath = path.join(containerURI, uuid.v1()); + } + if (!(fs.existsSync(newPath) || containerURI in usedURIs)) { + usedURIs[newPath] = true; + } else { + return null; + } + return newPath; +}; + +module.exports.releaseResourceUri = function(uri) { + delete usedURIs[uri]; +}; + +module.exports.verify = function(containerGraph, type) { + //TODO work on this method + var results = containerGraph.each(undefined, "a", type); + if (results.length === 1) { + return true; + } else { + return false; + } +}; + +module.exports.verifyDirectContainer = function(containerGraph) { + +}; + +module.exports.createNewResource = function(containerPath, containerGraph, + resourcePath, resourceGraph, resourceMetadata, callback) { + var containerURI = path.relative(options.fileBase, containerPath); + var resourceURI = path.relative(options.fileBase, resourcePath); + //TODO replace url with resource url + var rawResource = $rdf.serialize(undefined, + resourceGraph, options.baseUri + resourceURI, 'text/turtle'); + logging.log("Writing new resource to ", resourcePath); + logging.log(rawResource); + fs.writeFile(resourcePath, rawResource, writeResourceCallback); + + function writeResourceCallback(err) { + if (err) { + container.releaseResourceUri(resoucePath); + callback(err); + } else { + addUriTriple(containerGraph, containerURI, ldpVocab.contains, + resourceURI); + var rawContainer = $rdf.serialize(undefined, containerGraph, + options.uriBase, 'text/turtle'); + metadata.writeContainerMetadata(containerPath, rawContainer, + writeContainerCallback); + } + } + + function writeContainerCallback(err) { + if (err) { + module.exports.releaseResourceUri(resourcePath); + return callback(err); + } else { + metadata.writeMetadata(resourcePath, resourceMetadata, + writeMetadataCallback); + } + } + + function writeMetadataCallback(err) { + module.exports.releaseResourceUri(resourcePath); + return callback(err); + } +}; + +module.exports.createNewContainer = function(containerPath, containerGraph, + containerMetadata, callback) { + fs.mkdir(containerPath, mkdirCallback); + + function mkdirCallback(err) { + if (err) { + module.exports.releaseResourceUri(containerPath); + return callback(err); + } else { + var rawContainer = $rdf.serialize(undefined, containerGraph, + options.uriBase, 'text/turtle'); + metadata.writeContainerMetadata(containerPath, rawContainer, + writeContainerCallback); + } + } + + function writeContainerCallback(err) { + if (err) { + module.exports.releaseResourceUri(containerPath); + return callback(err); + } else { + metadata.writeMetadata(containerPath, containerMetadata, + writeMetadataCallback); + } + } + + function writeMetadataCallback(err) { + module.exports.releaseResourceUri(containerPath); + return callback(err); + } +}; diff --git a/fileStore.js b/fileStore.js new file mode 100644 index 000000000..b2acdc9ed --- /dev/null +++ b/fileStore.js @@ -0,0 +1,14 @@ +var path = require('path'); + +var options = require('./options.js'); +var logging = require('./logging.js'); + +module.exports.uriToFilename = function(uri) { + //if (uri.slice(0, options.pathStart.length) !== options.pathStart) { + //throw "Path '" + uri + "'not starting with base '" + options.pathStart + "'."; + //} + //var filename = options.fileBase + uri.slice(options.pathStart.length); + var filename = path.join(options.fileBase, uri); + logging.log(' -- filename ' +filename); + return filename; +}; diff --git a/handlers/delete.js b/handlers/delete.js new file mode 100644 index 000000000..0918dea38 --- /dev/null +++ b/handlers/delete.js @@ -0,0 +1,47 @@ +var fs = require('fs'); +var rimraf = require('rimraf'); + +var file = require('../fileStore.js'); +var logging = require('../logging.js'); +var metadata = require('../metadata.js'); + +module.exports.handler = function(req, res) { + logging.log('DELETE -- ' + req.path); + // res.header('MS-Author-Via' , 'SPARQL' ); + var filename = file.uriToFilename(req.path); + fs.stat(filename, function(err, stats) { + if (err) { + logging.log(" ### DELETE unlink() error: " + err); + return res.status(404).send("Can't delete file: " + err); + } else if (stats.isDirectory()) { + if (filename.charAt(filename.length - 1) !== '/') + filename += '/'; + metadata.deleteContainerMetadata(filename, containerCallback); + } else { + fs.unlink(filename, fileCallback); + } + }); + + function fileCallback(err) { + if (err) { + logging.log(" ### DELETE unlink() error: " + err); + return res.status(404).send("Can't delete file: " + err); // @@ best + } else { + //TODO remove file from container + metadata.deleteMetadata(filename, function(err) {}); + logging.log(" -- delete Ok " + req.text.length); + res.sendStatus(200); + } + } + + function containerCallback(err) { + if (err) { + logging.log("DELETE unlink() error: " + err); + return res.status(404).send("Can't delete container: " + err); + } else { + metadata.deleteMetadata(filename, function(err) {}); + logging.log(" -- delete Ok " + req.text.length); + res.sendStatus(200); + } + } +}; diff --git a/handlers/get.js b/handlers/get.js new file mode 100644 index 000000000..2705318ac --- /dev/null +++ b/handlers/get.js @@ -0,0 +1,84 @@ +var mime = require('mime'); +var fs = require('fs'); + +var header = require('../header.js'); +var metadata = require('../metadata.js'); +var options = require('../options.js'); +var logging = require('../logging.js'); +var file = require('../fileStore.js'); + +module.exports.handler = function(req, res){ + get(req, res, true); +}; + +module.exports.headHandler = function(req, res) { + logging.log("HEAD ssdss"); + get(req, res, false); +}; + +var get = function(req, res, includeBody) { + // Add request to subscription service + if (('' +req.path).slice(- options.changesSuffix.length) === + options.changesSuffix) { + logging.log("Subscribed to ", req.path); + return subscription.subscribeToChanges(req, res); + } + // Set headers + res.header('MS-Author-Via' , 'SPARQL' ); + // Note not yet in + // http://www.iana.org/assignments/link-relations/link-relations.xhtml + header.addLink(res, req.path + options.changesSuffix, 'changes' ); + if (includeBody) + logging.log('GET -- ' +req.path); + else + logging.log('HEAD -- ' +req.path); + var filename = file.uriToFilename(req.path); + fs.stat(filename, function(err, stats) { + if (err) { + logging.log(' -- read error ' + err); + res.status(404).send("Can't read file: "+ err); + } else if (stats.isDirectory()) { + if (includeBody) { + metadata.readContainerMetadata(filename, containerHandler); + } + else { + res.status(200).send(); + res.end(); + } + } else { + if (includeBody) + fs.readFile(filename, fileHandler); + else { + res.status(200).send(); + res.end(); + } + } + }); + + var fileHandler = function(err, data) { + if (err) { + logging.log(' -- read error ' + err); + res.status(404).send("Can't read file: "+ err); + } else { + logging.log(' -- read Ok ' + data.length); + ct = mime.lookup(filename); + res.set('content-type', ct); + logging.log(' -- content-type ' + ct); + res.send(data); + } + }; + + var containerHandler = function(err, rawContainer) { + if (err) { + res.status(404).send("Not a container"); + } else { + var containerGraph; + try { + // TODO: parse rawContainer to check is a valid graph + res.status(200).send(rawContainer); + } catch (parseErr) { + res.status(404).send("Not a valid container"); + } + } + }; +}; diff --git a/handlers/patch.js b/handlers/patch.js new file mode 100644 index 000000000..8b1c938dd --- /dev/null +++ b/handlers/patch.js @@ -0,0 +1,88 @@ +var mime = require('mime'); +var fs = require('fs'); +var $rdf = require('rdflib') + +var subscription = require('../subscription.js') +var options = require('../options.js') +var logging = require('../logging.js') + +module.exports.handler = function(req, res) { + logging.log('\nPOST ' +req.path); + logging.log(' text length: ' + (req.text ? req.text.length : 'undefined2')); + res.header('MS-Author-Via' , 'SPARQL' ); + var filename = uriToFilename(req.path); + patchType = req.get('content-type'); + fileType = mime.lookup(filename); + patchContentType = req.get('content-type'); + patchContentType = patchContentType.split(';')[0].trim(); // Ignore parameters + targetContentType = mime.lookup(filename); + var targetURI = options.prePathSlash + req.path; + var patchURI = targetURI ; // @@@ beware the triples from the patch ending up in the same place + logging.log("Patch Content-type " + patchContentType + " patching target " + targetContentType + " <" + targetURI + '>'); + var targetKB = $rdf.graph(); + var patchKB = $rdf.graph(); // Keep the patch in a sep KB as its URI is the same ! + + var fail = function(status, message) { + logging.log("FAIL "+status+ " " + message); + return res.status(status).send('\n'+ message+ '\n\n'); + } + switch(patchContentType) { + case 'application/sparql-update': + try { // Must parse relative to document's base address but patch doc should get diff URI + logging.log("parsing patch ..."); + var patch = $rdf.sparqlUpdateParser(req.text, patchKB, patchURI); + } catch(e) { + return res.status(400).send("Patch format syntax error:\n" + e + '\n'); + } + logging.log("reading target file ..."); + fs.readFile(filename, 'utf8', function (err,data) { + if (err) { + return res.status(404).send("Patch: Original file read error:" + + err + '\n'); + } + logging.log("File read OK "+data.length); + try { + logging.log("parsing target file ..."); + $rdf.parse(data, targetKB, targetURI, targetContentType); + logging.log("Target parsed OK "); + + } catch(e) { + return res.status(500).send("Patch: Target " + + targetContentType + " file syntax error:" + e); + } + + var target = patchKB.sym(targetURI); + + var writeFileBack = function() { + logging.log("Accumulated namespaces:" + targetKB.namespaces); + logging.log("Writeback URI base " + targetURI); + var data = $rdf.serialize(target, targetKB, targetURI, + targetContentType); + fs.writeFile(filename, data, 'utf8', function(err){ + if (err) { + return fail(500, + "Failed to write file back after patch: " + err); + } else { + logging.log("Patch applied OK"); + res.send("Patch applied OK\n"); + return subscription.publishDelta(req, res, + patchKB, targetURI); + }; + }); // end write done + }; + targetKB.applyPatch(patch, target, function(err){ + if (err) { + return fail(409, err); // HTTP inconsistency error -- very important. + } else { + writeFileBack(); + }; + }); + + }); // end read file done + break; + + default: + return fail(400, "Sorry unknowm patch content type: " + patchContentType) + }; // switch content-type +}; // postOrPatch + diff --git a/handlers/post.js b/handlers/post.js new file mode 100644 index 000000000..8780101be --- /dev/null +++ b/handlers/post.js @@ -0,0 +1,149 @@ +var path = require('path'); +var $rdf = require('rdflib'); + +var container = require('../container.js'); +var file = require('../fileStore.js'); +var header = require('../header.js'); +var logging = require('../logging.js'); +var metadata = require('../metadata.js'); + +var ldpVocab = require('../vocab/ldp.js'); +var rdfVocab = require('../vocab/rdf.js'); + +module.exports.handler = function(req, res) { + var containerPath = file.uriToFilename(req.path); + if (metadata.isMetadataFile(containerPath)) + return res.status(404).send(); + metadata.readMetadata(containerPath, metadataCallback); + + function metadataCallback(err, rawMetadata) { + if (err) { + res.sendStatus(404); + return; + } else { + var fileMetadata = metadata.parseMetadata(rawMetadata); + if (fileMetadata.isContainer) { + // Containers are always directories so it's safe to add a final slash + // if it does not exist already + if (containerPath.charAt(containerPath.length - 1) !== '/') + containerPath += '/'; + metadata.readContainerMetadata(containerPath, containerCallback); + } else { + res.set('Allow', 'GET,HEAD,PUT,DELETE').sendStatus(405); + return; + } + } + } + + function containerCallback(err, rawContainer) { + if (err) { + res.sendStatus(404); + return; + } + var contentType = ""; + if (req.is('text/turtle')) + contentType = 'text/turtle'; + else if (req.is('text/n3')) + contentType = 'text/n3'; + else if (req.is('application/rdf+xml')) + contentType = 'application/rdf+xml'; + else { + res.sendStatus(415); + return; + } + + var slug = req.get('Slug'); + var resourcePath = container.createResourceUri(containerPath, slug); + var resourceGraph = $rdf.graph(); + var containerGraph = $rdf.graph(); + + if (resourcePath === null) { + container.releaseResourceUri(resourcePath); + logging.log("URI already exists or in use"); + res.sendStatus(400); + return; + } + + try { + var resourceURI = path.resourceURI = path.relative(options.fileBase, + resourcePath); + logging.log(options.uriBase + resourceURI); + $rdf.parse(rawContainer, containerGraph, + options.uriBase + resourceURI, 'text/turtle'); + } catch (parseErr) { + logging.log(parseErr); + logging.log("Could not parse container:\n", rawContainer); + container.releaseResourceUri(resourcePath); + res.sendStatus(500); + return; + } + + try { + $rdf.parse(req.text, resourceGraph, options.uriBase, contentType); + } catch (parseErr) { + logging.log(req.text); + container.releaseResourceUri(resourcePath); + res.sendStatus(400); + return; + } + + var resourceMetadata = header.parseMetadataFromHeader(req.get('Link')); + header.addLinks(res, resourceMetadata); + var resourceType = ""; + + if (resourceMetadata.isBasicContainer) { + resourceMetadata.isContainer = true; + resourceType = ldpVocab.BasicContainer; + resourcePath += '/'; + } else if (resourceMetadata.isDirectContainer) { + resourceMetadata.isContainer = true; + resourceType = ldpVocab.DirectContainer; + } else { + resourceMetadata.isContainer = false; + resourceType = ldpVocab.Resource; + } + + resourceMetadata.isResource = true; + //TODO figure out if how to determine if RDFSource is true or false + resourceMetadata.isSourceResource = true; + + //TODO work on input verification. For now assume input is correct + //if (!container.verify(newResource, resourceType)) { + //container.releaseResourceUri(baseUri); + //res.send(400); + //return; + //} + + if (resourceMetadata.isBasicContainer) { + container.createNewContainer(resourcePath, resourceGraph, + resourceMetadata, function(err) { + if (err) { + res.sendStatus(500); + } else { + var resourceURI = path.relative(options.fileBase, + resourcePath); + res.set('Location', options.uriBase + resourceURI); + res.sendStatus(201); + } + return; + }); + } else if (!resourceMetadata.isContainer) { + container.createNewResource(containerPath, containerGraph, resourcePath, + resourceGraph, resourceMetadata, function(err) { + if (err) { + logging.log("Error creating resource:", err); + res.sendStatus(500); + } else { + var resourceURI = path.relative(options.fileBase, + resourcePath); + res.set('Location', options.uriBase + resourceURI); + res.sendStatus(201); + } + return; + }); + } else { + res.sendStatus(400); + return; + } + } +}; diff --git a/handlers/put.js b/handlers/put.js new file mode 100644 index 000000000..66c7036d0 --- /dev/null +++ b/handlers/put.js @@ -0,0 +1,38 @@ +var mime = require('mime'); +var fs = require('fs'); +var $rdf = require('rdflib'); + +var file = require('../fileStore.js'); +var header = require('../header.js'); +var metadata = require('../metadata.js'); +var options = require('../options.js'); +var logging = require('../logging.js'); + +module.exports.handler = function(req, res){ + logging.log('PUT ' + req.path); + logging.log('text length:' + (req.text ? req.text.length : 'undefined')); + res.header('MS-Author-Via' , 'SPARQL' ); + + var fileMetadata = header.parseMetadataFromHeader(req.get('Link')); + var filename = file.uriToFilename(req.path); + var ct1 = req.get('content-type'); + var ct2 = mime.lookup(filename); + + if (ct1 && ct2 && (ct1 !== ct2)) { + res.status(415).send("Content type mismatch with path file.extenstion"); + } + if (!ct2) { + res.status(415).send("Sorry, Filename must have extension for content type"); + } + + fs.writeFile(filename, req.text, function(err) { + if (err) { + logging.log(" ### Write error: " + err); + return res.status(500).send("Can't write file: "+ err); + } else { + logging.log(" -- write Ok " + req.text.length); + metadata.writeMetadata(filename, fileMetadata, function(err) {}); + res.send(); + } + }); // file write +}; diff --git a/header.js b/header.js new file mode 100644 index 000000000..672fc81c5 --- /dev/null +++ b/header.js @@ -0,0 +1,54 @@ +var li = require('li'); + +var logging = require('./logging.js'); +var metadata = require('./metadata.js'); +var ldpVocab = require('./vocab/ldp.js'); + +module.exports.addLink = function(res, value, rel) { + var oldLink = res.get('Link'); + if (oldLink === undefined) + res.set('Link', '<' + value + '>; rel=\'' + rel + '\''); + else + res.set('Link', oldLink + ', ' + '<' + value + + '>; rel=\'' + rel + '\''); +}; + +module.exports.addLinks = function(res, fileMetadata) { + if (fileMetadata.isResource) + module.exports.addLink(res, ldpVocab.Resource, 'type'); + if (fileMetadata.isSourceResource) + module.exports.addLink(res, ldpVocab.RDFSource, 'type'); + if (fileMetadata.isContainer) + module.exports.addLink(res, ldpVocab.Container, 'type'); + if (fileMetadata.isBasicContainer) + module.exports.addLink(res, ldpVocab.BasicContainer, 'type'); + if (fileMetadata.isDirectContainer) + module.exports.addLink(res, ldpVocab.DirectContainer, 'type'); +}; + +module.exports.parseMetadataFromHeader = function(linkHeader) { + var fileMetadata = new metadata.Metadata(); + if (linkHeader === undefined) + return fileMetadata; + var links = linkHeader.split(','); + for (var linkIndex in links) { + var link = links[linkIndex]; + var parsedLinks = li.parse(link); + console.log(parsedLinks, link); + for (var rel in parsedLinks) { + if (rel === 'type') { + if (parsedLinks[rel] === ldpVocab.Resource) + fileMetadata.isResource = true; + else if (parsedLinks[rel] === ldpVocab.RDFSource) + fileMetadata.isSourceResource = true; + else if (parsedLinks[rel] === ldpVocab.Container) + fileMetadata.isContainer = true; + else if (parsedLinks[rel] === ldpVocab.BasicContainer) + fileMetadata.isBasicContainer = true; + else if (parsedLinks[rel] === ldpVocab.DirectContainer) + fileMetadata.isDirectContainer = true; + } + } + } + return fileMetadata; +}; diff --git a/logging.js b/logging.js new file mode 100644 index 000000000..bc85ce4d0 --- /dev/null +++ b/logging.js @@ -0,0 +1,6 @@ +options = require('./options.js') + +module.exports.log = function() { + if (options.verbose) console.log.apply(console, arguments); +} + diff --git a/metadata.js b/metadata.js new file mode 100644 index 000000000..220cfdac8 --- /dev/null +++ b/metadata.js @@ -0,0 +1,118 @@ +// Metadata + +var fs = require('fs'); +var path = require('path'); + +var file = require('./fileStore.js'); +var header = require('./header.js'); +var logging = require('./logging.js'); +var ldpVocab = require('./vocab/ldp.js'); + +var metadataExtension = ".metadata"; +var containerExtension = ".container"; + +module.exports.Metadata = function() { + this.filename = ""; + this.isResource = false; + this.isSourceResource = false; + this.isContainer = false; + this.isBasicContainer = false; + this.isDirectContainer = false; +}; + +module.exports.isMetadataFile = function(filename) { + if (path.extname(filename) === metadataExtension || + path.extname(filename) === containerExtension) + return true; + return false; +}; + +module.exports.hasMetadata = function(filename) { + return fs.existsSync(filename + metadataExtension); +}; + +module.exports.parseMetadata = function(rawMetadata) { + var getProperty = function(object, property) { + if (object[property] !== undefined) return object[property]; + else throw new ReferenceError('Property does not exists'); + + }; + + try { + var jsonMetadata = JSON.parse(rawMetadata); + var fileMetadata = new module.exports.Metadata(); + fileMetadata.filename = getProperty(jsonMetadata, 'filename'); + fileMetadata.isResource = getProperty(jsonMetadata, 'isResource'); + fileMetadata.isSourceResource = getProperty(jsonMetadata, + 'isSourceResource'); + fileMetadata.isContainer = getProperty(jsonMetadata, + 'isContainer'); + fileMetadata.isBasicContainer = getProperty(jsonMetadata, + 'isBasicContainer'); + fileMetadata.isDirectContainer = getProperty(jsonMetadata, + 'isDirectContainer'); + return fileMetadata; + } catch (err) { + logging.log("Could not parse metadata from source"); + return Error("Invalid metadata"); + } +}; + +module.exports.writeMetadata = function(filename, metadata, callback) { + var rawMetadata = JSON.stringify(metadata); + fs.writeFile(filename + metadataExtension, rawMetadata, { + 'encoding': 'utf8' + }, callback); +}; + +module.exports.readMetadata = function(filename, callback) { + fs.readFile(filename + metadataExtension, { + 'encoding': 'utf8' + }, callback); +}; + +module.exports.deleteMetadata = function(filename, callback) { + fs.unlink(filename + metadataExtension, callback); +}; + +module.exports.hasContainerMetadata = function(directory) { + return fs.existsSync(directory + containerExtension); +}; + +module.exports.writeContainerMetadata = function(directory, container, callback) { + fs.writeFile(directory + containerExtension, container, callback); +}; + +module.exports.readContainerMetadata = function(directory, callback) { + fs.readFile(directory + containerExtension, { + 'encoding': 'utf8' + }, callback); +}; + +module.exports.deleteContainerMetadata = function(directory, callback) { + fs.unlink(directory + containerExtension, callback); +}; + +module.exports.linksHandler = function(req, res, next) { + var filename = file.uriToFilename(req.url); + filename = path.join(filename, req.path); + if (module.exports.isMetadataFile(filename)) { + res.send(404); + return; + } + var fileMetadata = new module.exports.Metadata(); + module.exports.readMetadata(filename, function(err, rawMetadata) { + if (err) { + fileMetadata.isResource = true; + } else { + try { + fileMetadata = module.exports.parseMetadata(rawMetadata); + } catch (parseErr) { + res.send(500); + return next(parseErr); + } + } + header.addLinks(res, fileMetadata); + next(); + }); +}; diff --git a/options.js b/options.js new file mode 100644 index 000000000..5893c6aae --- /dev/null +++ b/options.js @@ -0,0 +1,41 @@ +//Server options + +var logging = require('./logging.js'); +var regexp = require('node-regexp'); + +module.exports.aclSuffix = ""; +module.exports.uriBase = ""; +module.exports.fileBase = ""; +module.exports.address = ""; +module.exports.port = 3000; +module.exports.verbose = false; +module.exports.changesSuffix = ""; +module.exports.ssl = false; +module.exports.cors = false; +module.exports.pathStart = ""; +module.exports.prePathSlash = ""; +module.exports.pathFilter = ""; + +module.exports.init = function(argv) { + this.aclSuffix = argv.aclSuffix || process.env.ACLSUFFIX || ",acl"; + this.uriBase = argv.uriBase || process.env.URIBASE || + 'http://localhost:3000' + process.cwd() + '/test/'; + this.fileBase = argv.fileBase || process.env.FILEBASE || + process.cwd() + '/test/'; + this.address = argv.a || '0.0.0.0'; + this.port = parseInt(argv.p || process.env.PORT || 3000); + this.verbose = argv.v; + this.changesSuffix = argv.changesSuffix || ',changes'; + this.ssl = argv.S; + this.cors = argv.cors; + logging.log(" uriBase: " + this.uriBase); + this.pathStart = '/' + + this.uriBase.split('//')[1].split('/').slice(1).join('/'); + this.prePathSlash = this.uriBase.split('/') + .slice(0, 3).join('/'); + logging.log("URI pathStart: " + this.pathStart); + this.pathFilter = regexp().start(this.pathStart) + .toRegExp(); + logging.log("URI path filter regexp: " + this.pathFilter); + logging.log("Verbose: " + this.verbose); +}; diff --git a/package.json b/package.json index 822f9c8fe..e87f185d3 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,38 @@ { - "name": "ldp-httpd", - "description": "Read-write linked data web server based on express and rdflib.js", - "version": "0.1.0", - "private": false, - "author": { "name": "Tim Berners-Lee", "email": "timbl@w3.org" }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/linkeddata/node-ldp-httpd" - }, - "homepage": "http://github.com/linkeddata/node-ldp-httpd", - "bugs": "http://github.com/linkeddata/node-ldp-httpd/issues", - "dependencies": { - "express": "3.x", - "mime": "1.x", - "node-regexp": "", - "optimist": "0.6", - "raw-body": "1.x", - "rdflib": "git://github.com/linkeddata/rdflib.js.git", - "response-time": "2.x" - }, - "files": [ "server.js", "test", "Makefile"], - "main": "server.js", - "scripts": { - "start": "node server.js" - } + "name": "ldp-httpd", + "description": "Read-write linked data web server based on express and rdflib.js", + "version": "0.1.0", + "private": false, + "author": { + "name": "Tim Berners-Lee", + "email": "timbl@w3.org" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/linkeddata/node-ldp-httpd" + }, + "homepage": "http://github.com/linkeddata/node-ldp-httpd", + "bugs": "http://github.com/linkeddata/node-ldp-httpd/issues", + "dependencies": { + "express": "4.x", + "mime": "1.x", + "node-regexp": "", + "optimist": "0.6", + "raw-body": "1.x", + "rdflib": "0.1.0", + "response-time": "2.x", + "li": "1.x", + "node-uuid": "1.x", + "acl": "0.4.x" + }, + "files": [ + "server.js", + "test", + "Makefile" + ], + "main": "server.js", + "scripts": { + "start": "node server.js" + } } diff --git a/server.js b/server.js index 33464d0d5..976211827 100644 --- a/server.js +++ b/server.js @@ -1,50 +1,37 @@ // Read-Write Web: Linked Data Server -// - var express = require('express'); // See http://expressjs.com/guide.html var app = express(); - -var mime = require('mime'); -var fs = require('fs'); -var $rdf = require('rdflib.js') var responseTime = require('response-time'); // Add X-Response-Time headers -var path = require('path'); -var regexp = require('node-regexp'); - - - - -// Debugging: -// See http://stackoverflow.com/questions/1911015/how-to-debug-node-js-applications -// iff debug -//var agent = require('webkit-devtools-agent') -//agent.start() - -/* Install to your application, npm install webkit-devtools-agent -Include in your application, agent = require('webkit-devtools-agent') -Activate the agent: kill -SIGUSR2 -Access the agent via the appropriate link -*/ +var getRawBody = require('raw-body'); // See https://github.com/stream-utils/raw-body +var acl = require('./acl.js'); +var metadata = require('./metadata.js'); +var options = require('./options.js'); +var logging = require('./logging.js'); +var container = require('./container.js'); -/////////////////////////////////////////// ArgV handling ripped from http-server/bin/http-server +//Request handlers +var getHandler = require('./handlers/get.js'); +var postHandler = require('./handlers/post.js'); +var putHandler = require('./handlers/put.js'); +var deleteHandler = require('./handlers/delete.js'); +var patchHandler = require('./handlers/patch.js'); +// Command line handling var argv = require('optimist').boolean('cors').boolean('v').argv; if (argv.h || argv.help || argv['?']) { - console.log([ + console.log([ "usage: ldp-httpd [path] [options]", "", "options:", - " -p Port to use [3000]", - " -a Address to use [0.0.0.0]", -// " -d Show directory listings [true]", -// " -i Display autoIndex [true]", -// " -e --ext Default file extension if none supplied [none]", - " -v log messages to console", + " --uriBase Address, port, and default path of the server. (Example: http://localhost:3000/test/)", + " --fileBase Base location to serve resources. Requests whose paths do not have fileBase as a prefix will be ignored", + " -p Port to use", + " -v Log messages to console", + " --changesSuffix The suffix that will be used to identify the requests that will subscribe to changes to the object requested. Defaults to ,changes", " --cors Enable CORS via the 'Access-Control-Allow-Origin' header", -// " -o Open browser window after starting the server", " -c Set cache time (in seconds). e.g. -c10 for 10 seconds.", " To disable caching, use -c-1.", " --changesSuffix sss Change the URI suffix used for the URI of a change stream", @@ -58,332 +45,58 @@ if (argv.h || argv.help || argv['?']) { process.exit(); } -var options = { - aclSuffix: argv.aclSuffix || process.env.ACLSUFFIX || ",acl", - uriBase: argv.uriBase || process.env.URIBASE || 'http://localhost:3000'+process.cwd() + '/test/', - fileBase: argv.fileBase || procss.env.FILEBASE || process.cwd() + '/test/', - address: argv.a || '0.0.0.0', - port: parseInt(argv.p || process.env.PORT || 3000), - verbose: argv.v, - changesSuffix: argv.changesSuffix || ',changes', - ssl: argv.S, - cors: argv.cors -}; - - -var consoleLog = function() { - if (options.verbose) console.log.apply(console, arguments); -} - -var subscriptions = {}; // Map URI to array of watchers - -consoleLog(" uriBase: " + options.uriBase); -options.pathStart = '/' + options.uriBase.split('//')[1].split('/').slice(1).join('/'); -options.prePathSlash = options.uriBase.split('/').slice(0,3).join('/'); -consoleLog("URI pathStart: " + options.pathStart); -options.pathFilter = regexp().start(options.pathStart).toRegExp(); -consoleLog("URI path filter regexp: " + options.pathFilter); - - -consoleLog("Verbose: "+options.verbose); +// Initialize options +options.init(argv); +// Signal handling if (process.platform !== 'win32') { - // - // Signal handlers don't work on Windows. - // - process.on('SIGINT', function () { - consoleLog('http-server stopped.'); - process.exit(); - }); -}; - + // Signal handlers don't work on Windows. + process.on('SIGINT', function () { + logging.log('\nhttp-server stopped.'); + process.exit(); + }); +} -var PATCH = $rdf.Namespace('http://www.w3.org/ns/pim/patch#'); +container.createRootContainer(); -var uriToFilename = function(uri) { - if (uri.slice(0, options.pathStart.length) !== options.pathStart) { - throw "Path '"+uri+"'not starting with base '" + options.pathStart +"'."; - } - var filename = options.fileBase + uri.slice(options.pathStart.length); - consoleLog(' -- filename ' +filename); - return filename -}; +var router = express.Router(); +// Request handlers -var getRawBody = require('raw-body'); // See https://github.com/stream-utils/raw-body -app.use(function (req, res, next) { +// Extract raw body +router.use('/*', function (req, res, next) { getRawBody(req, { length: req.headers['content-length'], limit: '1mb', encoding: 'utf-8' // typer.parse(req.headers['content-type']).parameters.charset }, function (err, string) { - if (err) { - return next(err) - } - req.text = string - next() - }) -}); - - - - -var postOrPatch = function(req, res) { - consoleLog('\nPOST ' +req.path); - consoleLog(' text length: ' + (req.text ? req.text.length : 'undefined2')) - res.header('MS-Author-Via' , 'SPARQL' ); - var filename = uriToFilename(req.path); - patchType = req.get('content-type'); - fileType = mime.lookup(filename); - patchContentType = req.get('content-type'); - patchContentType = patchContentType.split(';')[0].trim(); // Ignore parameters - targetContentType = mime.lookup(filename); - var targetURI = options.prePathSlash + req.path; - var patchURI = targetURI ; // @@@ beware the triples from the patch ending up in the same place - consoleLog("Patch Content-type " + patchContentType + " patching target " + targetContentType + " <" + targetURI + '>'); - var targetKB = $rdf.graph(); - var patchKB = $rdf.graph(); // Keep the patch in a sep KB as its URI is the same ! - - var fail = function(status, message) { - consoleLog("FAIL "+status+ " " + message); - return res.status(status).send('\n'+ message+ '\n\n'); - } - switch(patchContentType) { - case 'application/sparql-update': - try { // Must parse relative to document's base address but patch doc should get diff URI - consoleLog("parsing patch ...") - var patch = $rdf.sparqlUpdateParser(req.text, patchKB, patchURI); - } catch(e) { - return res.status(400).send("Patch format syntax error:\n" + e + '\n'); - } - consoleLog("reading target file ...") - fs.readFile(filename, 'utf8', function (err,data) { - if (err) { - return res.status(404).send("Patch: Original file read error:" + err + '\n'); - } - consoleLog("File read OK "+data.length); - try { - consoleLog("parsing target file ...") - $rdf.parse(data, targetKB, targetURI, targetContentType); - consoleLog("Target parsed OK "); - - } catch(e) { - return res.status(500).send("Patch: Target " + targetContentType + " file syntax error:" + e); - } - - var target = patchKB.sym(targetURI); - - var writeFileBack = function() { - consoleLog("Accumulated namespaces:" + targetKB.namespaces) - consoleLog("Writeback URI base " + targetURI); - var data = $rdf.serialize(target, targetKB, targetURI, targetContentType); - // consoleLog("Writeback data: " + data); - - fs.writeFile(filename, data, 'utf8', function(err){ - if (err) { - return fail(500, "Failed to write file back after patch: "+ err); - } else { - consoleLog("Patch applied OK"); - res.send("Patch applied OK\n"); - return publishDelta(req, res, patchKB, targetURI); - }; - }); // end write done - }; - -/* - var performPatch = function(patch, targetKB, patchCallback) { // patchCallback(err) - - var doPatch = function(onDonePatch) { - consoleLog("doPatch ...") - - if (patch['delete']) { - consoleLog("doPatch delete "+patch['delete']) - var ds = patch['delete'] - if (bindings) ds = ds.substitute(bindings); - ds = ds.statements; - var bad = []; - var ds2 = ds.map(function(st){ // Find the actual statemnts in the store - var sts = targetKB.statementsMatching(st.subject, st.predicate, st.object, target); - if (sts.length === 0) { - consoleLog("NOT FOUND deletable " + st); - bad.push(st); - } else { - consoleLog("Found deletable " + st); - return sts[0] - } - }); - if (bad.length) { - return fail(409, "Couldn't find to delete: " + bad[0]) - } - ds2.map(function(st){ - targetKB.remove(st); - }); - }; - - if (patch['insert']) { - consoleLog("doPatch insert "+patch['insert']) - var ds = patch['insert']; - if (bindings) ds = ds.substitute(bindings); - ds = ds.statements; - ds.map(function(st){st.why = target; - consoleLog("Adding: " + st); - targetKB.add(st.subject, st.predicate, st.object, st.why)}); - }; - onDonePatch(); - }; - - var bindings = null; - if (patch.where) { - consoleLog("Processing WHERE: " + patch.where + '\n'); - - var query = new $rdf.Query('patch'); - query.pat = patch.where; - query.pat.statements.map(function(st){st.why = target}); - - var bindingsFound = []; - consoleLog("Processing WHERE - launching query: " + query.pat); - - targetKB.query(query, function onBinding(binding) { - bindingsFound.push(binding) - }, - targetKB.fetcher, - function onDone() { - if (bindingsFound.length == 0) { - return patchCallback("No match found to be patched:" + patch.where); - } - if (bindingsFound.length > 1) { - return patchCallback("Patch ambiguous. No patch done."); - } - bindings = bindingsFound[0]; - doPatch(patchCallback); - }); - } else { - doPatch(patchCallback) - }; - }; - */ - targetKB.applyPatch(patch, target, function(err){ - if (err) { - return fail(409, err); // HTTP inconsistency error -- very important. - } else { - writeFileBack(); - }; - }); - - }); // end read file done - break; - - default: - return fail(400, "Sorry unknowm patch content type: " + patchContentType) - }; // switch content-type -}; // postOrPatch - - -var subscribeToChanges = function(req, res) { - var targetPath = req.path.slice(0, - options.changesSuffix.length); // lop off ',changes' - if (subscriptions[targetPath] === undefined) { - subscriptions[targetPath] = []; - } - subscriptions[targetPath].push({ 'request': req, 'response': res}); - res.set('content-type', 'text/n3'); - res.setTimeout(0); // Disable timeout (does this work??) - consoleLog("\nGET CHANGES: Now " + subscriptions[targetPath].length + " subscriptions for " + targetPath); -} - -var publishDelta = function (req, res, patchKB, targetURI){ - if (! subscriptions[req.path]) return; - var target = $rdf.sym(targetURI); // @@ target below - var data = $rdf.serialize(undefined, patchKB, targetURI, 'text/n3'); - consoleLog("-- Distributing change to " + req.path); -// consoleLog(" change is NT: <<<<<" + patchKB.toNT() + ">>>>>\n"); -// consoleLog(" change is why: <<<<<" + patchKB.statements.map(function(st){ -// return st.why.toNT()}).join(', ') + ">>>>>\n"); - consoleLog(" change is: <<<<<" + data + ">>>>>\n"); - - subscriptions[req.path].map(function(subscription){ - subscription.response.write(data); - // foo.pipe(subscription.response, { 'end': false}); -// subscription.response.send(data) - - }); - -} - - -app.use(responseTime()); - - -//////////////////// Request handlers: - -app.get(options.pathFilter, function(req, res){ - - if (('' +req.path).slice(- options.changesSuffix.length) === options.changesSuffix) - return subscribeToChanges(req, res); - - res.header('MS-Author-Via' , 'SPARQL' ); - // Note not yet in http://www.iana.org/assignments/link-relations/link-relations.xhtml - res.header('Link' , '' + req.path + options.changesSuffix + ' ; rel=changes' ); - consoleLog('GET -- ' +req.path); - var filename = uriToFilename(req.path); - fs.readFile(filename, function(err, data) { if (err) { - consoleLog(' -- read error ' + err); - res.status(404).send("Can't read file: "+ err) - } else { - consoleLog(' -- read Ok ' + data.length); - ct = mime.lookup(filename); - res.set('content-type', ct) - consoleLog(' -- content-type ' + ct); - res.send(data); - }; + return next(err); + } + req.text = string; + next(); }); }); -app.put(options.pathFilter, function(req, res){ - consoleLog('PUT ' +req.path); - consoleLog(' text length:' + (req.text ? req.text.length : 'undefined1')) - res.header('MS-Author-Via' , 'SPARQL' ); - var filename = uriToFilename(req.path); - ct1 = req.get('content-type'); - ct2 = mime.lookup(filename); - if (ct1 && ct2 && (ct1 !== ct2)) { - res.status(415).send("Content type mismatch with path file.extenstion"); - } - if (!ct2) { // @@ Later, add the extension or track metadata - res.status(415).send("Sorry, Filename must have extension for content type"); - } - fs.writeFile(filename, req.text, function(err) { - if (err) { - consoleLog(" ### Write error: " + err); - return res.status(500).send("Can't write file: "+ err); - } else { - consoleLog(" -- write Ok " + req.text.length); - res.send(); - } - }); // file write -}); +// Add linkds headers +router.use(metadata.linksHandler); -app.delete(options.pathFilter, function(req, res){ - consoleLog('DELETE ' +req.path); -// res.header('MS-Author-Via' , 'SPARQL' ); - var filename = uriToFilename(req.path); - fs.unlink(filename, function(err) { - if (err) { - consoleLog(" ### DELETE unlink() error: " + err); - return res.status(404).send("Can't delete file: "+ err); // @@ best - } else { - consoleLog(" -- delete Ok " + req.text.length); - res.send(); - } - }); // file delete -}); +// Add response time +router.use(responseTime()); +router.use(acl.aclHandler); -app.post(options.pathFilter, postOrPatch); -app.patch(options.pathFilter, postOrPatch); +// HTTP methods handlers +router.get('/*', getHandler.handler); +router.head('/*', getHandler.headHandler); +router.put('/*', putHandler.handler); +router.delete('/*', deleteHandler.handler); +router.post('/*', postHandler.handler); +router.patch('/*', patchHandler.handler); +app.use(options.pathStart, router); + +//Start server var server = app.listen(options.port, function() { - consoleLog('Listening on port %d', server.address().port); + logging.log('Listening on port %d', server.address().port); }); - - diff --git a/subscription.js b/subscription.js new file mode 100644 index 000000000..c863ed3dd --- /dev/null +++ b/subscription.js @@ -0,0 +1,28 @@ +var $rdf = require('rdflib') + +module.exports.subscriptions = {}; // Map URI to array of watchers + +module.exports.subscribeToChanges = function(req, res) { + // lop off ',changes' + var targetPath = req.path.slice(0, - options.changesSuffix.length); + if (module.exports.subscriptions[targetPath] === undefined) { + module.exports.subscriptions[targetPath] = []; + } + module.exports.subscriptions[targetPath].push({ 'request': req, 'response': res}); + res.set('content-type', 'text/n3'); + res.setTimeout(0); // Disable timeout (does this work??) + consoleLog("\nGET CHANGES: Now " + + module.exports.subscriptions[targetPath].length + + " subscriptions for " + targetPath); +} + +module.exports.publishDelta = function (req, res, patchKB, targetURI) { + if (! module.exports.subscriptions[req.path]) return; + var target = $rdf.sym(targetURI); // @@ target below + var data = $rdf.serialize(undefined, patchKB, targetURI, 'text/n3'); + consoleLog("-- Distributing change to " + req.path); + consoleLog(" change is: <<<<<" + data + ">>>>>\n"); + module.exports.subscriptions[req.path].map(function(subscription) { + subscription.response.write(data); + }); +} diff --git a/vocab/ldp.js b/vocab/ldp.js new file mode 100644 index 000000000..53db1caef --- /dev/null +++ b/vocab/ldp.js @@ -0,0 +1,48 @@ +/* + * Copyright 2014 IBM Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function define(name, value) { + Object.defineProperty(exports, name, { + value: value, + enumerable: true + }); +} + +var ns = 'http://www.w3.org/ns/ldp#'; +define('ns', ns); +define('prefix', 'ldp'); + +// Resources +define('Resource', ns + 'Resource'); +define('RDFSource', ns + 'RDFSource'); +define('Container', ns + 'Container'); +define('BasicContainer', ns + 'BasicContainer'); +define('DirectContainer', ns + 'DirectContainer'); + +// Properties +define('contains', ns + 'contains'); +define('membershipResource', ns + 'membershipResource'); +define('hasMemberRelation', ns + 'hasMemberRelation'); +define('isMemberOfRelation', ns + 'isMemberOfRelation'); + +// Link relations +define('constrainedBy', ns + 'constrainedBy'); + +// Preferences +define('PreferContainment', ns + 'PreferContainment'); +define('PreferMembership', ns + 'PreferMembership'); +define('PreferMinimalContainer', ns + 'PreferMinimalContainer'); +define('PreferEmptyContainer', ns + 'PreferEmptyContainer'); diff --git a/vocab/rdf.js b/vocab/rdf.js new file mode 100644 index 000000000..1898814ce --- /dev/null +++ b/vocab/rdf.js @@ -0,0 +1,31 @@ +/* + * Copyright 2014 IBM Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function define(name, value) { + Object.defineProperty(exports, name, { + value: value, + enumerable: true + }); +} + +var ns = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; +define('ns', ns); +define('prefix', 'rdf'); + +// Resources + +// Properties +define('type', ns + 'type');