├── .cfignore ├── .gitignore ├── LICENSE ├── NOTICE ├── Procfile ├── README.md ├── app.js ├── config.json ├── context.json ├── db.js ├── env.js ├── jsonld.js ├── manifest.yml ├── media.js ├── package.json ├── project.json ├── public ├── constraints.html ├── folder.png ├── index.html └── style.css ├── screenshot.png ├── server.js ├── service.js ├── testng ├── LDPjs-bc.xml └── LDPjs-dc-simple.xml ├── turtle.js ├── viz.js └── vocab ├── ldp.js └── rdf.js /.cfignore: -------------------------------------------------------------------------------- 1 | launchConfigurations/ 2 | .git/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | launchConfigurations/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | LDP.js Linked Data Platform Reference Implementation 2 | Copyright 2014 IBM Corporation. 3 | 4 | This product includes software developed at IBM (http://www.ibm.com/). 5 | 6 | Portions of this software were adapted from the Eclipse Lyo project 7 | (http://eclipse.org/lyo). 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LDP.js 2 | 3 | A simple reference implementation for the [W3C Linked Data 4 | Platform](http://www.w3.org/2012/ldp), leveraging Node.js, 5 | MongoDB, and a few other js libraries. Catch it running at 6 | [http://ldpjs.mybluemix.net](http://ldpjs.mybluemix.net). 7 | 8 | ![LDP.js Screenshot](screenshot.png "LDP.js Screenshot") 9 | 10 | LDP.js supports LDP basic and direct containers. Indirect 11 | containers and non-RDF source are not implemented. 12 | 13 | ## Running 14 | 15 | First, install [Node.js](http://nodejs.org). Next, install and start 16 | [MongoDB](http://docs.mongodb.org/manual/installation/). 17 | 18 | To start the app, run these commands 19 | 20 | $ npm install 21 | $ node app.js 22 | 23 | Finally, point your browser to 24 | [http://localhost:3000/](http://localhost:3000/). 25 | 26 | ## License 27 | 28 | Licensed under the Apache License, Version 2.0 (the "License"); 29 | you may not use this file except in compliance with the License. 30 | You may obtain a copy of the License at 31 | 32 | http://www.apache.org/licenses/LICENSE-2.0 33 | 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. 39 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Initializes MongoDB and starts the app. The main application logic is in 19 | * service.js. 20 | */ 21 | 22 | var express = require('express'); 23 | var env = require('./env.js'); 24 | 25 | console.log("configuration:"); 26 | console.dir(env); 27 | 28 | // setup middleware 29 | var app = express(); 30 | app.use(express.static(__dirname + '/public')); 31 | 32 | // fill in full request URL 33 | app.use(function(req, res, next) { 34 | req.fullURL = env.appBase + req.originalUrl; 35 | next(); 36 | }); 37 | 38 | // fill in req.rawBody 39 | app.use(function(req, res, next) { 40 | req.rawBody = ''; 41 | req.setEncoding('utf8'); 42 | 43 | req.on('data', function(chunk) { 44 | req.rawBody += chunk; 45 | }); 46 | 47 | req.on('end', function() { 48 | next(); 49 | }); 50 | }); 51 | 52 | // initialize database and set up LDP services and viz when ready 53 | var db = require('./db.js'); 54 | db.init(env, function(err) { 55 | if (err) { 56 | console.error(err); 57 | console.error("Can't initialize MongoDB."); 58 | } else { 59 | require('./service.js')(app, db, env); 60 | require('./viz.js')(app, db, env); 61 | } 62 | }); 63 | 64 | // error handling 65 | app.use(function(err, req, res, next){ 66 | console.error(err.stack); 67 | res.send(500, 'Something broke!'); 68 | }); 69 | 70 | // Start server 71 | app.listen(env.listenPort, env.listenHost); 72 | console.log('App started on port ' + env.listenPort); 73 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "scheme": "http", 3 | "host": "localhost", 4 | "port": 3000, 5 | "context": "/r", 6 | "mongoURL": "mongodb://localhost:27017/ldp" 7 | } 8 | -------------------------------------------------------------------------------- /context.json: -------------------------------------------------------------------------------- 1 | { 2 | "ldp": "http://www.w3.org/ns/ldp#", 3 | "dcterms": "http://purl.org/dc/terms/", 4 | "foaf": "http://xmlns.com/foaf/0.1/" 5 | } 6 | -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | /* 19 | * db.js stores RDF graphs in MongoDB. Each document representations 20 | * an RDF graph. Documents have the properties below. The 'triples' 21 | * property is the RDF. Other properties are metadata. 22 | * 23 | * All documents: 24 | * 25 | * name - the URI of the graph 26 | * interactionModel - the URI indicating the LDP interaction model of the resource 27 | * container - the container for this resource 28 | * deleted - boolean indicating if the resource has been deleted (to avoid reusing URIs) 29 | * triples - an array of RDF triples in N3.js format 30 | * 31 | * Direct containers: 32 | * 33 | * membershipResource - the ldp:membershipResource property 34 | * hasMemberRelation - the ldp:hasMemberRelation property 35 | * isMemberOfRelation - the ldp:isMemberOfRelation property 36 | * 37 | * Membership resources: 38 | * 39 | * membershipResourceFor - the associated direct container (always 1:1) 40 | * 41 | * Rather than storing a link to all of its members in the container, 42 | * we have a property in each resource that points back to its 43 | * container. On a container GET, we query for a container's resources 44 | * and mix in containment triples. 45 | */ 46 | 47 | var ldp = require('./vocab/ldp.js'); // LDP vocabulary 48 | 49 | var db; 50 | 51 | function graphs() { 52 | return db.collection('graphs'); 53 | } 54 | 55 | // index the graph name for fast lookups and uniqueness 56 | function ensureIndex() { 57 | graphs().ensureIndex({ 58 | name: 1 59 | }, { 60 | unique: true 61 | }, function(err) { 62 | if (err) { 63 | // not fatal, but log the error 64 | console.log(err.stack); 65 | } 66 | }); 67 | } 68 | 69 | exports.init = function(env, callback) { 70 | require('mongodb').connect(env.mongoURL, function(err, conn) { 71 | if (err) { 72 | callback(err); 73 | return; 74 | } 75 | 76 | db = conn; 77 | exports.graphs = graphs(); 78 | console.log("Connected to MongoDB at: "+env.mongoURL); 79 | ensureIndex(); 80 | callback(); 81 | }); 82 | }; 83 | 84 | exports.drop = function(callback) { 85 | graphs().drop(callback); 86 | ensureIndex(); 87 | exports.graphs = graphs(); 88 | }; 89 | 90 | exports.reserveURI = function(uri, callback) { 91 | // simply create a document with only a URI. we will just update it later on put 92 | // if it fails, we reject the uri 93 | graphs().insert({ 94 | name: uri 95 | }, function(err, result) { 96 | callback(err); 97 | }); 98 | }; 99 | 100 | exports.releaseURI = function(uri) { 101 | graphs().remove({ 102 | name: uri 103 | }, function(err) { 104 | if (err) { 105 | console.log(err.stack); 106 | } 107 | }); 108 | }; 109 | 110 | exports.put = function(doc, callback) { 111 | console.log('db.put'); 112 | console.dir(doc); 113 | graphs().update({ 114 | name: doc.name 115 | }, doc, { 116 | upsert: true, 117 | safe: true 118 | }, callback); 119 | }; 120 | 121 | exports.get = function(uri, callback) { 122 | console.log('db.get'); 123 | graphs().find({ 124 | name: uri 125 | }, { 126 | limit: 1 127 | }).toArray(function(err, docs) { 128 | if (docs && docs[0]) { 129 | console.dir(docs[0]); 130 | callback(err, docs[0]); 131 | } else { 132 | callback(err); 133 | } 134 | }); 135 | }; 136 | 137 | exports.remove = function(uri, callback) { 138 | graphs().update({ 139 | name: uri, 140 | }, { 141 | $set: { 142 | deleted: true, 143 | containedBy: null, 144 | triples: [] 145 | } 146 | }, { 147 | safe: true 148 | }, callback); 149 | }; 150 | 151 | exports.findContainer = function(uri, callback) { 152 | graphs().find({ 153 | name: uri, 154 | $or: [{ 155 | interactionModel: ldp.BasicContainer 156 | }, { 157 | interactionModel: ldp.DirectContainer 158 | }], 159 | deleted: { 160 | $ne: true 161 | } 162 | }, { 163 | name: 1, 164 | interactionModel: 1, 165 | membershipResource: 1, 166 | hasMemberRelation: 1, 167 | isMemberOfRelation: 1 168 | }, { 169 | limit: 1 170 | }).toArray(function(err, docs) { 171 | callback(err, (docs && docs.length) ? docs[0] : null); 172 | }); 173 | }; 174 | 175 | exports.getContainment = function(uri, callback) { 176 | graphs().find({ 177 | containedBy: uri, 178 | deleted: { 179 | $ne: true 180 | } 181 | }, { 182 | name: 1 183 | }).toArray(function(err, docs) { 184 | var result = []; 185 | if (docs) { 186 | docs.forEach(function(doc) { 187 | result.push(doc.name); 188 | }); 189 | } 190 | 191 | callback(err, result); 192 | }); 193 | }; 194 | 195 | exports.createMembershipResource = function(document, callback) { 196 | graphs().update({ 197 | name: document.membershipResource 198 | }, { 199 | $push: { 200 | membershipResourceFor: { 201 | container: document.name, 202 | hasMemberRelation: document.hasMemberRelation 203 | } 204 | } 205 | }, { 206 | upsert: true, 207 | safe: true 208 | }, callback); 209 | }; 210 | -------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Looks at environment variables for app configuration (base URI, port, LDP 19 | * context, etc.), falling back to what's in confg.json. 20 | */ 21 | 22 | var url = require("url"); 23 | var config = require('./config.json'); 24 | 25 | // the IP address of the Cloud Foundry DEA (Droplet Execution Agent) that hosts this application: 26 | exports.listenHost = (process.env.VCAP_APP_HOST || process.env.OPENSHIFT_NODEJS_IP || config.host); 27 | 28 | // the port on the DEA for communication with the application: 29 | exports.listenPort = (process.env.VCAP_APP_PORT || process.env.OPENSHIFT_NODEJS_PORT || config.port); 30 | 31 | function addSlash(url) { 32 | if (url.substr(-1) == '/') { 33 | return url; 34 | } else { 35 | return url + '/'; 36 | } 37 | } 38 | 39 | function toURL(urlObj) { 40 | if ((urlObj.scheme === 'http' && urlObj.port === 80) || 41 | (urlObj.scheme === 'https' && urlObj.port === 443)) { 42 | delete urlObj.port; 43 | } 44 | 45 | return url.format(urlObj); 46 | } 47 | 48 | // scheme, host, port, and base URI 49 | var appInfo = JSON.parse(process.env.VCAP_APPLICATION || "{}"); 50 | if (process.env.LDP_BASE) { 51 | // LDP_BASE env var set 52 | exports.ldpBase = addSlash(process.env.LDP_BASE); 53 | var url = url.parse(exports.ldpBase); 54 | exports.scheme = url.scheme; 55 | exports.host = url.host; 56 | exports.port = url.port; 57 | exports.context = url.pathname; 58 | exports.appBase = toURL({ 59 | protocol: exports.scheme, 60 | host: exports.host, 61 | port: exports.port 62 | }); 63 | } else { 64 | // no LDP_BASE set 65 | exports.scheme = (process.env.VCAP_APP_PORT) ? 'http' :config.scheme; 66 | if (appInfo.application_uris) { 67 | exports.host = appInfo.application_uris[0]; 68 | } else { 69 | exports.host = process.env.HOSTNAME || config.host; 70 | } 71 | 72 | // public port is the default in a Bluemix environment 73 | if (!process.env.VCAP_APP_PORT) { 74 | exports.port = config.port; 75 | } 76 | exports.context = addSlash(config.context); 77 | 78 | exports.appBase = toURL({ 79 | protocol: exports.scheme, 80 | hostname: exports.host, 81 | port: exports.port 82 | }); 83 | 84 | exports.ldpBase = toURL({ 85 | protocol: exports.scheme, 86 | hostname: exports.host, 87 | port: exports.port, 88 | pathname: exports.context 89 | }); 90 | } 91 | 92 | // MongoDB 93 | if (process.env.VCAP_SERVICES) { 94 | var env = JSON.parse(process.env.VCAP_SERVICES); 95 | exports.mongoURL = env.mongolab[0].credentials.uri; 96 | } else { 97 | if (process.env.OPENSHIFT_MONGODB_DB_URL) { 98 | exports.mongoURL = process.env.OPENSHIFT_MONGODB_DB_URL; 99 | } else { 100 | exports.mongoURL = process.env.MONGO_URL || config.mongoURL; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /jsonld.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Parses and serializes JSON-LD (application/ld+json) to and from an array of 19 | * triples, the triple format used by N3.js. 20 | */ 21 | 22 | var N3 = require('n3'); 23 | var jsonld = require('jsonld'); 24 | var media = require('./media.js'); // media types 25 | var ldp = require('./vocab/ldp.js'); // LDP vocabulary 26 | var rdf = require('./vocab/rdf.js'); // RDF vocabulary 27 | var context = require('./context.json'); // our JSON-LD context 28 | 29 | exports.parse = function (req, resourceURI, callback) { 30 | var json = JSON.parse(req.rawBody); 31 | jsonld.toRDF(json, { base: resourceURI }, function(err, dataset) { 32 | if (err) { 33 | callback(err); 34 | return; 35 | } 36 | 37 | // transform the dataset to the N3.js triples format we use in our database 38 | // both libraries use a different internal format unfortunately 39 | var result = []; 40 | for(var graphName in dataset) { 41 | var triples = dataset[graphName]; 42 | // FIXME: what about graph names? 43 | triples.forEach(function (triple) { 44 | var next = {}; 45 | next.subject = triple.subject.value; 46 | next.predicate = triple.predicate.value; 47 | if (triple.object.type === 'IRI' || triple.object.type === 'blank node') { 48 | next.object = triple.object.value; 49 | } else { 50 | var literal = '"' + triple.object.value + '"'; 51 | if (triple.object.language) { 52 | literal += '@' + triple.object.language; 53 | } else if (triple.object.datatype && triple.object.datatype !== 'http://www.w3.org/2001/XMLSchema#string') { 54 | literal += '^^<' + triple.object.datatype + '>'; 55 | } 56 | next.object = literal; 57 | } 58 | 59 | result.push(next); 60 | }); 61 | } 62 | 63 | callback(null, result); 64 | }); 65 | }; 66 | 67 | function jsonldResource(subject) { 68 | return { '@id': subject }; 69 | } 70 | 71 | function jsonldObject(object) { 72 | if (N3.Util.isUri(object) || N3.Util.isBlank(object)) { 73 | return jsonldResource(object); 74 | } 75 | 76 | var result = {}; 77 | var value = N3.Util.getLiteralValue(object); 78 | result['@value'] = value; 79 | var type = N3.Util.getLiteralType(object); 80 | if (type && type !== 'http://www.w3.org/2001/XMLSchema#string') { 81 | result['@type'] = type; 82 | } 83 | var language = N3.Util.getLiteralLanguage(object); 84 | if (language) { 85 | result['@language'] = language; 86 | } 87 | 88 | return result; 89 | } 90 | 91 | exports.serialize = function(triples, callback) { 92 | var resources = []; 93 | var map = {}; 94 | 95 | triples.forEach(function(triple) { 96 | var sub = map[triple.subject]; 97 | if (!sub) { 98 | sub = jsonldResource(triple.subject); 99 | map[triple.subject] = sub; 100 | resources.push(sub); 101 | } 102 | 103 | var object; 104 | if ((N3.Util.isUri(triple.object) || N3.Util.isBlank(triple.object)) && !map[triple.object]) { 105 | object = jsonldResource(triple.object); 106 | } 107 | 108 | if (triple.predicate === rdf.type) { 109 | jsonld.addValue(sub, '@type', triple.object, { propertyIsArray: true }); 110 | return; 111 | } 112 | 113 | object = jsonldObject(triple.object); 114 | jsonld.addValue(sub, triple.predicate, object, { propertyIsArray: true }); 115 | }); 116 | 117 | jsonld.compact(resources, context, function(err, json) { 118 | if (err) { 119 | callback(err); 120 | } 121 | 122 | var content = JSON.stringify(json, undefined, 4); 123 | callback(null, media.jsonld, content); 124 | }); 125 | }; 126 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | applications: 2 | - disk_quota: 1024M 3 | host: ldpjs 4 | name: LDP.js 5 | command: node app.js 6 | path: . 7 | domain: mybluemix.net 8 | instances: 1 9 | memory: 128M 10 | -------------------------------------------------------------------------------- /media.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Media type constants. 19 | */ 20 | 21 | function define(name, value) { 22 | Object.defineProperty(exports, name, { 23 | value: value, 24 | enumerable: true 25 | }); 26 | } 27 | 28 | define('turtle', 'text/turtle'); 29 | define('text', 'text/plain'); 30 | define('n3', 'text/n3'); 31 | define('jsonld', 'application/ld+json'); 32 | define('json', 'application/json'); 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LDP.js", 3 | "version": "0.0.1", 4 | "description": "A Node.js reference app for LDP", 5 | "dependencies": { 6 | "express": "4.14.0", 7 | "n3": "0.2.7", 8 | "jsonld": "0.4.11", 9 | "mongodb": "2.2.5" 10 | }, 11 | "engines": { 12 | "node": "0.10.26" 13 | 14 | }, 15 | "scripts": { 16 | "start": "node app.js" 17 | }, 18 | "repository": {} 19 | 20 | } 21 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | {"Name":"LDPjs"} 2 | -------------------------------------------------------------------------------- /public/constraints.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | 21 | 22 | 23 | LDP.js 24 | 25 | 26 | 27 | 28 |

LDP.js

29 | 30 |

LDP.js is an LDP server built using Node.js and MongoDB. It accepts most any RDF, 33 | but does have a few constraints:

34 | 35 | 44 | 45 |
DISCLAIMER: This service is provided as-is. It could go 46 | down at any time and change without warning. In fact, it might have 47 | already changed while you were reading this. The data behind this 48 | services will get scrubbed periodically. If this service gets abused, 49 | wse will either take it down or restrict access. Play nice.
50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /public/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spadgett/LDPjs/9ab1dba9ea42ab1a187a2c3718f9a618fa50ade7/public/folder.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | 21 | 22 | Linked Data Platform Reference Implementation 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | W3C 33 | 34 |

Linked Data Platform Reference Implementation

35 |

This impl is an experiment using Node.JS and 36 | MongoDB, hosted on a PaaS, to build and host an LDP reference 37 | impl.

38 |

Enjoy. Oh, and here's the source.

39 | 40 |
41 |
42 | 43 |
44 | GET | 45 | PUT | 46 | POST | 47 | DELETE 48 |
49 | 50 |
51 |
52 |
53 | 54 | 58 | 59 |
60 |
61 |
Loading...
62 |
63 | 64 | 76 | 77 | 98 | 99 | 106 | 107 | 108 |
DISCLAIMER: This service is provided as-is. It could go 109 | down at any time and change without warning. In fact, it might have 110 | already changed while you were reading this. The data behind this 111 | services will get scrubbed periodically. If this service gets abused, 112 | we will either take it down or restrict access. Play nice.
113 | 114 |
115 | 116 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | html { 18 | color: #000; 19 | background: #FFF; 20 | } 21 | 22 | body { 23 | font-family: "Helvetica Neue",Helvetica,Arial,Verdana,Geneva,sans-serif; 24 | color: #333; 25 | } 26 | 27 | .tabs { 28 | margin-bottom: 0.5em; 29 | } 30 | 31 | .fixed { 32 | font-family:monospace; 33 | padding: 1em; 34 | border: 1px dashed #2f6fab; 35 | color: black; 36 | background-color: #f9f9f9; 37 | line-height: 1.1em; 38 | white-space: pre; 39 | overflow-x: scroll; 40 | margin-top: 0.5em; 41 | } 42 | 43 | .urlInput { 44 | width: 40em; 45 | } 46 | 47 | .message { 48 | margin-bottom: 0.5em; 49 | } 50 | 51 | .textArea { 52 | display: block; 53 | width: 100%; 54 | height: 20em; 55 | -webkit-box-sizing: border-box; 56 | -moz-box-sizing: border-box; 57 | box-sizing: border-box; 58 | margin-bottom: 0.5em; 59 | } 60 | 61 | .error { 62 | color: red; 63 | } 64 | 65 | circle { 66 | stroke: #fff; 67 | stroke-width: 1.5px; 68 | } 69 | 70 | .link { 71 | stroke: #999; 72 | stroke-opacity: .6; 73 | } 74 | 75 | text { 76 | fill: #000; 77 | font: 10px sans-serif; 78 | pointer-events: none; 79 | } 80 | 81 | label { 82 | font-size: 80%; 83 | } 84 | 85 | input[type=checkbox] { 86 | vertical-align: middle; 87 | position: relative; 88 | bottom: 1px; 89 | } 90 | 91 | blockquote { 92 | border: 1px solid #E0CB52; 93 | background-color: #FCFAEE; 94 | padding: 5px; 95 | } 96 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spadgett/LDPjs/9ab1dba9ea42ab1a187a2c3718f9a618fa50ade7/screenshot.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require("./app.js"); 2 | -------------------------------------------------------------------------------- /service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * service.js handles HTTP requests for LDP resources. 19 | */ 20 | 21 | module.exports = function(app, db, env) { 22 | var ldp = require('./vocab/ldp.js'); // LDP vocabulary 23 | var rdf = require('./vocab/rdf.js'); // RDF vocabulary 24 | var media = require('./media.js'); // media types 25 | var turtle = require('./turtle.js'); // text/turtle parsing and serialization 26 | var jsonld = require('./jsonld.js'); // application/ld+json parsing and serialization 27 | var crypto = require('crypto'); // for MD5 (ETags) 28 | 29 | // create root container if it doesn't exist 30 | db.get(env.ldpBase, function(err, document) { 31 | if (err) { 32 | console.log(err.stack); 33 | return; 34 | } 35 | 36 | if (!document || document.deleted) { 37 | createRootContainer(function(err) { 38 | if (err) { 39 | console.log(err.stack); 40 | } 41 | }); 42 | } 43 | }); 44 | 45 | // route any requests matching the LDP context (defaults to /r/*) 46 | var resource = app.route(env.context + '*'); 47 | resource.all(function(req, res, next) { 48 | // all responses should have Link: rel=type 49 | var links = { 50 | type: ldp.Resource 51 | }; 52 | // also include implementation constraints 53 | links[ldp.constrainedBy] = env.appBase + '/constraints.html'; 54 | res.links(links); 55 | next(); 56 | }); 57 | 58 | function get(req, res, includeBody) { 59 | res.set('Vary', 'Accept'); 60 | db.get(req.fullURL, function(err, document) { 61 | if (err) { 62 | console.log(err.stack); 63 | res.sendStatus(500); 64 | return; 65 | } 66 | 67 | if (!document) { 68 | res.sendStatus(404); 69 | return; 70 | } 71 | 72 | if (document.deleted) { 73 | res.sendStatus(410); 74 | return; 75 | } 76 | 77 | // determine what format to serialize using the Accept header 78 | var serialize; 79 | if (req.accepts(media.turtle)) { 80 | serialize = turtle.serialize; 81 | } else if (req.accepts(media.jsonld) || req.accepts(media.json)) { 82 | serialize = jsonld.serialize; 83 | } else { 84 | res.sendStatus(406); 85 | return; 86 | } 87 | 88 | // add common response headers 89 | addHeaders(res, document); 90 | 91 | // some triples like containment are calculated on-the-fly rather 92 | // than being stored in the document 93 | // insertCalculatedTriples also looks at the Prefer header to see 94 | // what to include 95 | insertCalculatedTriples(req, document, function(err, preferenceApplied) { 96 | if (err) { 97 | console.log(err.stack); 98 | res.sendStatus(500); 99 | return; 100 | } 101 | 102 | serialize(document.triples, function(err, contentType, content) { 103 | if (err) { 104 | console.log(err.stack); 105 | res.sendStatus(500); 106 | return; 107 | } 108 | 109 | if (preferenceApplied) { 110 | res.set('Preference-Applied', 'return=representation'); 111 | } 112 | 113 | // generate an ETag for the content 114 | var eTag = getETag(content); 115 | if (req.get('If-None-Match') === eTag) { 116 | res.sendStatus(304); 117 | return; 118 | } 119 | 120 | res.writeHead(200, { 121 | 'ETag': eTag, 122 | 'Content-Type': contentType 123 | }); 124 | if (includeBody) { 125 | res.end(new Buffer(content), 'utf-8'); 126 | } else { 127 | res.end(); 128 | } 129 | }); 130 | }); 131 | }); 132 | } 133 | 134 | resource.get(function(req, res, next) { 135 | console.log('GET ' + req.path); 136 | get(req, res, true); 137 | }); 138 | 139 | resource.head(function(req, res, next) { 140 | console.log('HEAD ' + req.path); 141 | get(req, res, false); 142 | }); 143 | 144 | // allow dropping the database using DELETE /db 145 | // not recommended for production servers ;) 146 | app.delete('/db', function(req, res, next) { 147 | db.drop(function(err) { 148 | if (err) { 149 | console.log(err.stack); 150 | res.sendStatus(500); 151 | } else { 152 | createRootContainer(function(err) { 153 | if (err) { 154 | console.log(err.stack); 155 | res.sendStatus(500); 156 | } 157 | 158 | res.sendStatus(204); 159 | }); 160 | } 161 | }); 162 | }); 163 | 164 | function putUpdate(req, res, document, newTriples, serialize) { 165 | if (isContainer(document)) { 166 | res.set('Allow', 'GET,HEAD,DELETE,OPTIONS,POST').sendStatus(405); 167 | return; 168 | } 169 | 170 | var ifMatch = req.get('If-Match'); 171 | if (!ifMatch) { 172 | res.sendStatus(428); 173 | return; 174 | } 175 | 176 | // add membership triples if necessary to calculate the correct ETag 177 | insertCalculatedTriples(null, document, function(err) { 178 | if (err) { 179 | console.log(err.stack); 180 | res.sendStatus(500); 181 | return; 182 | } 183 | 184 | if (req.is(media.turtle)) { 185 | serialize = turtle.serialize; 186 | } else { 187 | serialize = jsonld.serialize; 188 | } 189 | 190 | // calculate the ETag from the matching representation 191 | serialize(document.triples, function(err, contentType, content) { 192 | if (err) { 193 | console.log(err.stack); 194 | res.sendStatus(500); 195 | return; 196 | } 197 | 198 | var eTag = getETag(content); 199 | if (ifMatch !== eTag) { 200 | res.sendStatus(412); 201 | return; 202 | } 203 | 204 | // remove any containment triples from the request body if this 205 | // is a container. then update the document with the new 206 | // triples. we store containment with the resources 207 | // themselves, not in the container document. 208 | document.triples = newTriples; 209 | 210 | // determine if there are changes to the interaction model 211 | updateInteractionModel(document); 212 | 213 | // remove any membership triples if this is a membership 214 | // resource so we don't store them directly 215 | removeMembership(document); 216 | 217 | db.put(document, function(err) { 218 | if (err) { 219 | console.log(err.stack); 220 | res.sendStatus(500); 221 | return; 222 | } 223 | 224 | res.sendStatus(204); 225 | }); 226 | }); 227 | }); 228 | } 229 | 230 | function putCreate(req, res, triples) { 231 | var document = { 232 | name: req.fullURL, 233 | triples: triples 234 | }; 235 | updateInteractionModel(document); 236 | 237 | // check if the client requested a specific interaction model through a 238 | // Link header. if so, override what we found from the RDF content. 239 | // FIXME: look for Link type=container as well 240 | if (hasResourceLink(req)) { 241 | document.interactionModel = ldp.RDFSource; 242 | } 243 | 244 | // check the membership triple pattern if this is a direct container 245 | if (!isMembershipPatternValid(document)) { 246 | res.sendStatus(409); 247 | return; 248 | } 249 | 250 | db.put(document, function(err) { 251 | if (err) { 252 | console.log(err.stack); 253 | res.sendStatus(500); 254 | return; 255 | } 256 | 257 | // create a membership resource if necessary. 258 | createMembershipResource(document, function(err) { 259 | if (err) { 260 | console.log(err.stack); 261 | db.releaseURI(loc); 262 | res.sendStatus(500); 263 | return; 264 | } 265 | 266 | res.sendStatus(201); 267 | }); 268 | }); 269 | } 270 | 271 | resource.put(function(req, res, next) { 272 | console.log('PUT ' + req.path); 273 | var parse, serialize; 274 | if (req.is(media.turtle)) { 275 | parse = turtle.parse; 276 | serialize = turtle.serialize; 277 | } else if (req.is(media.jsonld) || req.is(media.json)) { 278 | parse = jsonld.parse; 279 | serialize = jsonld.serialize; 280 | } else { 281 | res.sendStatus(415); 282 | return; 283 | } 284 | 285 | parse(req, req.fullURL, function(err, newTriples) { 286 | if (err) { 287 | res.sendStatus(400); 288 | return; 289 | } 290 | 291 | // get the resource to check if it exists and check its ETag 292 | db.get(req.fullURL, function(err, document) { 293 | if (err) { 294 | console.log(err.stack); 295 | res.sendStatus(500); 296 | } 297 | 298 | if (document) { 299 | if (document.deleted) { 300 | res.sendStatus(410); 301 | return; 302 | } 303 | 304 | // the resource exists. update it 305 | putUpdate(req, res, document, newTriples, serialize); 306 | } else { 307 | putCreate(req, res, newTriples); 308 | } 309 | }); 310 | }); 311 | }); 312 | 313 | resource.post(function(req, res, next) { 314 | console.log('POST ' + req.path); 315 | db.findContainer(req.fullURL, function(err, container) { 316 | if (err) { 317 | console.log(err.stack); 318 | res.sendStatus(500); 319 | return; 320 | } 321 | 322 | if (!container) { 323 | res.set('Allow', 'GET,HEAD,PUT,DELETE,OPTIONS').sendStatus(405); 324 | return; 325 | } 326 | 327 | var parse; 328 | if (req.is(media.turtle)) { 329 | parse = turtle.parse; 330 | } else if (req.is(media.jsonld) || req.is(media.json)) { 331 | parse = jsonld.parse; 332 | } else { 333 | res.sendStatus(415); 334 | return; 335 | } 336 | 337 | assignURI(req.fullURL, req.get('Slug'), function(err, loc) { 338 | if (err) { 339 | console.log(err.stack); 340 | res.sendStatus(500); 341 | return; 342 | } 343 | 344 | parse(req, loc, function(err, triples) { 345 | if (err) { 346 | // allow the URI to be used again 347 | db.releaseURI(loc); 348 | res.sendStatus(400); 349 | return; 350 | } 351 | 352 | var document = { 353 | name: loc, 354 | containedBy: req.fullURL, 355 | triples: triples 356 | }; 357 | 358 | updateInteractionModel(document); 359 | addHeaders(res, document); 360 | 361 | // check if the client requested a specific interaction model through a Link header 362 | // if so, override what we found from the RDF content 363 | // FIXME: look for Link type=container as well 364 | if (hasResourceLink(req)) { 365 | document.interactionModel = ldp.RDFSource; 366 | } 367 | 368 | // check the membership triple pattern if this is a direct container 369 | if (!isMembershipPatternValid(document)) { 370 | db.releaseURI(loc); 371 | res.sendStatus(409); 372 | return; 373 | } 374 | 375 | // add the "inverse" isMemberOfRelation link if needed 376 | if (container.interactionModel === ldp.DirectContainer && 377 | container.isMemberOfRelation) { 378 | document.triples.push({ 379 | subject: loc, 380 | predicate: container.isMemberOfRelation, 381 | object: req.fullURL 382 | }); 383 | } 384 | 385 | // create the resource 386 | db.put(document, function(err) { 387 | if (err) { 388 | console.log(err.stack); 389 | db.releaseURI(loc); 390 | res.sendStatus(500); 391 | return; 392 | } 393 | 394 | // create a membership resource if necessary. 395 | createMembershipResource(document, function(err) { 396 | if (err) { 397 | console.log(err.stack); 398 | db.releaseURI(loc); 399 | res.sendStatus(500); 400 | return; 401 | } 402 | 403 | res.location(loc).sendStatus(201); 404 | }); 405 | }); 406 | }); 407 | }); 408 | }); 409 | }); 410 | 411 | resource.delete(function(req, res, next) { 412 | console.log('DELETE: ' + req.path); 413 | db.remove(req.fullURL, function(err, result) { 414 | if (err) { 415 | console.log(err.stack); 416 | res.sendStatus(500); 417 | return; 418 | } 419 | 420 | res.sendStatus(result ? 204 : 404); 421 | }); 422 | }); 423 | 424 | resource.options(function(req, res, next) { 425 | db.get(req.fullURL, function(err, document) { 426 | if (err) { 427 | console.log(err.stack); 428 | res.sendStatus(500); 429 | return; 430 | } 431 | 432 | if (!document) { 433 | res.sendStatus(404); 434 | return; 435 | } 436 | 437 | if (document.deleted) { 438 | res.sendStatus(410); 439 | return; 440 | } 441 | 442 | addHeaders(res, document); 443 | res.sendStatus(200); 444 | }); 445 | }); 446 | 447 | // creates a root container on first run 448 | function createRootContainer(callback) { 449 | var triples = [{ 450 | subject: env.ldpBase, 451 | predicate: rdf.type, 452 | object: ldp.Resource 453 | }, { 454 | subject: env.ldpBase, 455 | predicate: rdf.type, 456 | object: ldp.RDFSource 457 | }, { 458 | subject: env.ldpBase, 459 | predicate: rdf.type, 460 | object: ldp.Container 461 | }, { 462 | subject: env.ldpBase, 463 | predicate: rdf.type, 464 | object: ldp.BasicContainer 465 | }, { 466 | subject: env.ldpBase, 467 | predicate: 'http://purl.org/dc/terms/title', 468 | object: '"LDP.js root container"' 469 | }]; 470 | 471 | db.put({ 472 | name: env.ldpBase, 473 | interactionModel: ldp.BasicContainer, 474 | triples: triples, 475 | deleted: false 476 | }, callback); 477 | } 478 | 479 | // create a membership resource for the container if it's a direct 480 | // container and the membership resource is not the container itself 481 | function createMembershipResource(document, callback) { 482 | if (document.interactionModel === ldp.DirectContainer && 483 | document.membershipResource && 484 | document.membershipResource !== document.name) { 485 | // create membership resource 486 | db.createMembershipResource(document, callback); 487 | } else { 488 | callback(); 489 | } 490 | } 491 | 492 | // generate an ETag for a response using an MD5 hash 493 | // note: insert any calculated triples before calling getETag() 494 | function getETag(content) { 495 | return 'W/"' + crypto.createHash('md5').update(content).digest('hex') + '"'; 496 | } 497 | 498 | // add common headers to all responses 499 | function addHeaders(res, document) { 500 | var allow = 'GET,HEAD,DELETE,OPTIONS'; 501 | if (isContainer(document)) { 502 | res.links({ 503 | type: document.interactionModel 504 | }); 505 | allow += ',POST'; 506 | res.set('Accept-Post', media.turtle + ',' + media.jsonld + ',' + media.json); 507 | } else { 508 | allow += ',PUT'; 509 | } 510 | 511 | res.set('Allow', allow); 512 | } 513 | 514 | // checks if document represents a basic or direct container 515 | // this is set using document.interactionModel and can't be changed 516 | // we don't look at the RDF type 517 | function isContainer(document) { 518 | return document.interactionModel === ldp.BasicContainer || document.interactionModel === ldp.DirectContainer; 519 | } 520 | 521 | // look at the triples to determine the type of container if this is a 522 | // container and, if a direct container, its membership pattern 523 | function updateInteractionModel(document) { 524 | var interactionModel = ldp.RDFSource; 525 | document.triples.forEach(function(triple) { 526 | var s = triple.subject, 527 | p = triple.predicate, 528 | o = triple.object; 529 | if (s !== document.name) { 530 | return; 531 | } 532 | 533 | // determine the interaction model from the RDF type 534 | // direct takes precedence if the resource has both direct and basic RDF types 535 | if (p === rdf.type && interactionModel !== ldp.DirectContainer && (o === ldp.BasicContainer || o === ldp.DirectContainer)) { 536 | interactionModel = o; 537 | return; 538 | } 539 | 540 | if (p === ldp.membershipResource) { 541 | document.membershipResource = o; 542 | return; 543 | } 544 | 545 | if (p === ldp.hasMemberRelation) { 546 | document.hasMemberRelation = o; 547 | } 548 | 549 | if (p === ldp.isMemberOfRelation) { 550 | document.isMemberOfRelation = o; 551 | } 552 | }); 553 | 554 | // don't override an existing interaction model 555 | if (!document.interactionModel) { 556 | document.interactionModel = interactionModel; 557 | } 558 | } 559 | 560 | // determine if this is a membership resource. if it is, insert the 561 | // membership triples. 562 | function insertMembership(req, document, callback) { 563 | var patterns = document.membershipResourceFor; 564 | if (patterns) { 565 | if (hasPreferOmit(req, ldp.PreferMembership)) { 566 | callback(null, true); // preference applied 567 | return; 568 | } 569 | 570 | // respond with Preference-Applied: return=representation if 571 | // membership was explicitly requested 572 | var preferenceApplied = hasPreferInclude(req, ldp.PreferMembership); 573 | var inserted = 0; 574 | patterns.forEach(function(pattern) { 575 | db.getContainment(pattern.container, function(err, containment) { 576 | if (err) { 577 | callback(err); 578 | return; 579 | } 580 | 581 | if (containment) { 582 | containment.forEach(function(resource) { 583 | document.triples.push({ 584 | subject: document.name, 585 | predicate: pattern.hasMemberRelation, 586 | object: resource 587 | }); 588 | }); 589 | } 590 | 591 | if (++inserted === patterns.length) { 592 | callback(null, preferenceApplied); 593 | } 594 | }); 595 | }); 596 | } else { 597 | callback(null, false); 598 | } 599 | } 600 | 601 | // insert any dynamically calculated triples 602 | function insertCalculatedTriples(req, document, callback) { 603 | // insert membership if this is a membership resource 604 | insertMembership(req, document, function(err, preferenceApplied) { 605 | if (err) { 606 | callback(err); 607 | return; 608 | } 609 | 610 | // next insert any dynamic triples if this is a container 611 | if (!isContainer(document)) { 612 | callback(null, preferenceApplied); 613 | return; 614 | } 615 | 616 | // check if client is asking for a minimal container 617 | var minimal = false; 618 | if (hasPreferInclude(req, ldp.PreferMinimalContainer) || 619 | hasPreferInclude(req, ldp.PreferEmptyContainer)) { 620 | preferenceApplied = true; 621 | minimal = true; 622 | } 623 | 624 | // include containment? 625 | var includeContainment; 626 | if (hasPreferInclude(req, ldp.PreferContainment)) { 627 | includeContainment = true; 628 | preferenceApplied = true; 629 | } else if (hasPreferOmit(req, ldp.PreferContainment)) { 630 | includeContainment = false; 631 | preferenceApplied = true; 632 | } else { 633 | includeContainment = !minimal; 634 | } 635 | 636 | // include membership? 637 | var includeMembership; 638 | if (document.interactionModel === ldp.DirectContainer && document.hasMemberRelation) { 639 | if (hasPreferInclude(req, ldp.PreferMembership)) { 640 | includeMembership = true; 641 | preferenceApplied = true; 642 | } else if (hasPreferOmit(req, ldp.PreferMembership)) { 643 | includeMembership = false; 644 | preferenceApplied = true; 645 | } else { 646 | includeMembership = !minimal; 647 | } 648 | } else { 649 | includeMembership = false; 650 | } 651 | 652 | if (!includeContainment && !includeMembership) { 653 | // we're done! 654 | callback(null, preferenceApplied); 655 | return; 656 | } 657 | 658 | db.getContainment(document.name, function(err, containment) { 659 | if (err) { 660 | callback(err); 661 | return; 662 | } 663 | 664 | if (containment) { 665 | containment.forEach(function(resource) { 666 | if (includeContainment) { 667 | document.triples.push({ 668 | subject: document.name, 669 | predicate: ldp.contains, 670 | object: resource 671 | }); 672 | } 673 | 674 | if (includeMembership) { 675 | document.triples.push({ 676 | subject: document.membershipResource, 677 | predicate: document.hasMemberRelation, 678 | object: resource 679 | }); 680 | } 681 | }); 682 | } 683 | 684 | callback(null, preferenceApplied); 685 | }); 686 | }); 687 | } 688 | 689 | // append 'path' to the end of a uri 690 | // - any query or hash in the uri is removed 691 | // - any special characters like / and ? in 'path' are replaced 692 | function addPath(uri, path) { 693 | uri = uri.split("?")[0].split("#")[0]; 694 | if (uri.substr(-1) !== '/') { 695 | uri += '/'; 696 | } 697 | 698 | // remove special characters from the string (e.g., '/', '..', '?') 699 | var lastSegment = path.replace(/[^\w\s\-_]/gi, ''); 700 | return uri + encodeURIComponent(lastSegment); 701 | } 702 | 703 | // generates and reserves a unique URI with base URI 'container' 704 | function uniqueURI(container, callback) { 705 | var candidate = addPath(container, 'res' + Date.now()); 706 | db.reserveURI(candidate, function(err) { 707 | callback(err, candidate); 708 | }); 709 | } 710 | 711 | // reserves a unique URI for a new resource. will use slug if available, 712 | // but falls back to the usual naming scheme if slug is already used 713 | function assignURI(container, slug, callback) { 714 | if (slug) { 715 | var candidate = addPath(container, slug); 716 | db.reserveURI(candidate, function(err) { 717 | if (err) { 718 | uniqueURI(container, callback); 719 | } else { 720 | callback(null, candidate); 721 | } 722 | }); 723 | } else { 724 | uniqueURI(container, callback); 725 | } 726 | } 727 | 728 | // removes any membership triples from a membership resource before updating 729 | // it in the database 730 | // membership triples are not stored with the resource itself (see db.js) 731 | function removeMembership(document) { 732 | if (document.membershipResourceFor) { 733 | // find the member relations. handle the case where the resource is 734 | // a membership resource for more than one container. 735 | var memberRelations = {}; 736 | document.membershipResourceFor.forEach(function(memberPattern) { 737 | if (memberPattern.hasMemberRelation) { 738 | memberRelations[memberPattern.hasMemberRelation] = 1; 739 | } 740 | }); 741 | 742 | // now filter the triples 743 | document.triples = document.triples.filter(function(triple) { 744 | // keep the triple if the subject is not the membership 745 | // resource or the predicate is not one of the member relations 746 | return triple.subject !== document.name || !memberRelations[triple.predicate]; 747 | }); 748 | } 749 | } 750 | 751 | // look for a Link request header indicating the entity uses a ldp:Resource 752 | // interaction model rather than container 753 | function hasResourceLink(req) { 754 | var link = req.get('Link'); 755 | // look for links like 756 | // ; rel="type" 757 | // these are also valid 758 | // ;rel=type 759 | // ; rel="type http://example.net/relation/other" 760 | return link && 761 | /\s*;\s*rel\s*=\s*(("\s*([^"]+\s+)*type(\s+[^"]+)*\s*")|\s*type[\s,;$])/ 762 | .test(link); 763 | } 764 | 765 | function hasPreferInclude(req, inclusion) { 766 | return hasPrefer(req, 'include', inclusion); 767 | } 768 | 769 | function hasPreferOmit(req, omission) { 770 | return hasPrefer(req, 'omit', omission); 771 | } 772 | 773 | function hasPrefer(req, token, parameter) { 774 | if (!req) { 775 | return false; 776 | } 777 | 778 | var preferHeader = req.get('Prefer'); 779 | if (!preferHeader) { 780 | return false; 781 | } 782 | 783 | // from the LDP prefer parameters, the only charcter we need to escape 784 | // for regular expressions is '.' 785 | // https://dvcs.w3.org/hg/ldpwg/raw-file/default/ldp.html#prefer-parameters 786 | var word = parameter.replace(/\./g, '\\.'); 787 | 788 | // construct a regex that matches the preference 789 | var regex = 790 | new RegExp(token + '\\s*=\\s*("\\s*([^"]+\\s+)*' + word + '(\\s+[^"]+)*\\s*"|' + word + '$)'); 791 | return regex.test(preferHeader); 792 | } 793 | 794 | // check the consistency of the membership triple pattern if this is a direct container 795 | function isMembershipPatternValid(document) { 796 | if (document.interactionModel !== ldp.DirectContainer) { 797 | // not a direct container, nothing to do 798 | return true; 799 | } 800 | 801 | // must have a membership resouce 802 | if (!document.membershipResource) { 803 | return false; 804 | } 805 | 806 | // must have hasMemberRelation or isMemberOfRelation, but can't have both 807 | if (document.hasMemberRelation) { 808 | return !document.isMemberOfRelation; 809 | } 810 | if (document.isMemberOfRelation) { 811 | return !document.hasMemberRelation; 812 | } 813 | 814 | // no membership triple pattern 815 | return false; 816 | } 817 | }; 818 | -------------------------------------------------------------------------------- /testng/LDPjs-bc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /testng/LDPjs-dc-simple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /turtle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Parses and serializes Turtle (text/turtle) using N3.js. 19 | */ 20 | 21 | var N3 = require('n3'); 22 | var url = require('url'); 23 | var path = require('path'); 24 | var media = require('./media.js'); // media types 25 | var ldp = require('./vocab/ldp.js'); // LDP vocabulary 26 | var rdf = require('./vocab/rdf.js'); // RDF vocabulary 27 | 28 | // normalize paths (remove '.' and '..') 29 | function normalize(urlStr) { 30 | var urlObj = url.parse(urlStr); 31 | urlObj.pathname = path.normalize(urlObj.pathname); 32 | return urlObj.format(); 33 | } 34 | 35 | exports.serialize = function(triples, callback) { 36 | var writer = N3.Writer(); 37 | writer.addTriples(triples); 38 | writer.end(function(err, content) { 39 | callback(err, media.turtle, content); 40 | }); 41 | }; 42 | 43 | exports.parse = function(req, resourceURI, callback) { 44 | var parser = N3.Parser({ documentURI: resourceURI }), triples = []; 45 | parser.parse(req.rawBody, function(err, triple) { 46 | if (err) { 47 | callback(err); 48 | } else if (triple) { 49 | triple.subject = normalize(triple.subject); 50 | if (N3.Util.isUri(triple.object)) { 51 | triple.object = normalize(triple.object); 52 | } 53 | triples.push(triple); 54 | } else { 55 | // when last triple is null, we're done parsing 56 | callback(null, triples); 57 | } 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /viz.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Transform the data in the RDF store to the JSON format for a D3.js 19 | * force-directed graph (see public/index.html). 20 | */ 21 | 22 | module.exports = function(app, db, env) { 23 | var rdf = require('./vocab/rdf.js'); 24 | var ldp = require('./vocab/ldp.js'); 25 | 26 | app.get('/v', function(req, res, next) { 27 | console.log('GET ' + req.path); 28 | 29 | db.graphs.find({ 30 | deleted: { 31 | $ne: true 32 | }, 33 | 'triples.0': { 34 | $exists: true 35 | } 36 | }, { 37 | limit: 1000, // put a reasonable limit on how many we'll process 38 | sort: 'name' // try to get the containers (at least the root container) 39 | }).toArray(function(err, docs) { 40 | if (err) { 41 | console.log(err.stack); 42 | res.send(500); 43 | return; 44 | } 45 | 46 | var nodes = {}; 47 | var types = {"none":0}; 48 | var nextGroupIdx = 1; 49 | var jsonRes = { nodes: [], links: [] }; 50 | 51 | // First build the array of all nodes (resources/graphs), 52 | // keep track of array indexes 53 | docs.forEach(function(d){ 54 | var node = { 55 | name: nodeName(d.name), 56 | group: 0 57 | }; 58 | var l = jsonRes.nodes.push(node); 59 | nodes[node.name] = l-1; 60 | }); 61 | 62 | // Next find all the arcs between resources 63 | docs.forEach(function(d) { 64 | var subName = nodeName(d.name); 65 | if (nodes[subName] === undefined) { 66 | return; 67 | } 68 | 69 | // Add in containment triples, which are stored with the resource 70 | if (d.containedBy) { 71 | var containerName = nodeName(d.containedBy); 72 | if (nodes[containerName] !== undefined) { 73 | jsonRes.links.push({ 74 | value: 1, // Always 1, why? 75 | source: nodes[containerName], 76 | target: nodes[subName] 77 | }); 78 | } 79 | } 80 | 81 | d.triples.forEach(function(t) { 82 | var objName = nodeName(t.object); 83 | if (nodes[objName] !== undefined) { 84 | jsonRes.links.push({ 85 | value: 1, // Always 1, why? 86 | source: nodes[subName], 87 | target: nodes[objName] 88 | }); 89 | } 90 | 91 | // Assign a group based on rdf:type, but ignore some 92 | // generic LDP types like ldp:RDFSource 93 | if (t.predicate === rdf.type && 94 | t.object !== ldp.Resource && 95 | t.object !== ldp.RDFSource) { 96 | if (!types[t.object]) { 97 | types[t.object] = nextGroupIdx++; 98 | } 99 | jsonRes.nodes[nodes[subName]].group = types[t.object]; 100 | if (t.object === ldp.Container) { 101 | jsonRes.nodes[nodes[subName]].img = 'folder.png'; 102 | console.log("Found a container " + subName); 103 | } 104 | } 105 | }); 106 | }); 107 | res.json(jsonRes); 108 | }); 109 | 110 | }); 111 | 112 | function nodeName(uri) { 113 | return uri.replace(env.appBase, ''); 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /vocab/ldp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | function define(name, value) { 18 | Object.defineProperty(exports, name, { 19 | value: value, 20 | enumerable: true 21 | }); 22 | } 23 | 24 | var ns = 'http://www.w3.org/ns/ldp#'; 25 | define('ns', ns); 26 | define('prefix', 'ldp'); 27 | 28 | // Resources 29 | define('Resource', ns + 'Resource'); 30 | define('RDFSource', ns + 'RDFSource'); 31 | define('Container', ns + 'Container'); 32 | define('BasicContainer', ns + 'BasicContainer'); 33 | define('DirectContainer', ns + 'DirectContainer'); 34 | 35 | // Properties 36 | define('contains', ns + 'contains'); 37 | define('membershipResource', ns + 'membershipResource'); 38 | define('hasMemberRelation', ns + 'hasMemberRelation'); 39 | define('isMemberOfRelation', ns + 'isMemberOfRelation'); 40 | 41 | // Link relations 42 | define('constrainedBy', ns + 'constrainedBy'); 43 | 44 | // Preferences 45 | define('PreferContainment', ns + 'PreferContainment'); 46 | define('PreferMembership', ns + 'PreferMembership'); 47 | define('PreferMinimalContainer', ns + 'PreferMinimalContainer'); 48 | define('PreferEmptyContainer', ns + 'PreferEmptyContainer'); 49 | -------------------------------------------------------------------------------- /vocab/rdf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 IBM Corporation. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | function define(name, value) { 18 | Object.defineProperty(exports, name, { 19 | value: value, 20 | enumerable: true 21 | }); 22 | } 23 | 24 | var ns = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; 25 | define('ns', ns); 26 | define('prefix', 'rdf'); 27 | 28 | // Resources 29 | 30 | // Properties 31 | define('type', ns + 'type'); 32 | --------------------------------------------------------------------------------