├── .gitignore ├── .idea ├── .gitignore ├── vcs.xml ├── misc.xml ├── modules.xml └── oslc-client.iml ├── .vscode └── launch.json ├── index.js ├── .github ├── dependabot.yml └── workflows │ ├── node.js.yml │ └── deno.yml ├── package.json ├── examples ├── simpleQuery.js ├── oslcClientExample.js ├── simpleRMRead.js ├── simpleCMRead.js └── updateCR.js ├── __tests__ ├── OSLCClient.tests.js └── rdflib.integration.test.js ├── RootServices.js ├── ServiceProviderCatalog.js ├── Compact.js ├── namespaces.js ├── README.md ├── ServiceProvider.js ├── OSLCResource.js ├── LICENSE.txt └── OSLCClient.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | out 3 | .DS_Store 4 | /coverage/ 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/oslc-client.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/index.js" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /index.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 | 'use strict'; 18 | 19 | // This module is the entry point for the OSLC Client library. 20 | // It exports the server module, which is the main module of the library. 21 | // This is what is imported when the module is required. 22 | export { OSLCClient } from './OSLCClient.js'; -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | open-pull-requests-limit: 99 6 | schedule: 7 | interval: "daily" 8 | groups: 9 | deps-angular: 10 | patterns: 11 | - "@angular/*" 12 | update-types: 13 | - "minor" 14 | - "patch" 15 | deps-typescript: 16 | patterns: 17 | - "ts-*" 18 | - "tslib" 19 | - "tslint" 20 | - "typescript" 21 | update-types: 22 | - "minor" 23 | - "patch" 24 | deps-testing: 25 | patterns: 26 | - "karma*" 27 | - "karma-*" 28 | - "jasmine-*" 29 | - "@types/jasmine*" 30 | deps-webcomponents: 31 | patterns: 32 | - "@webcomponents/*" 33 | - package-ecosystem: "github-actions" # See documentation for possible values 34 | directory: "/" # Location of package manifests 35 | open-pull-requests-limit: 99 36 | schedule: 37 | interval: "daily" 38 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 15 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node-version: [18.x, 20.x, 22.x, 24.x] 22 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 23 | 24 | steps: 25 | - uses: actions/checkout@v6 26 | 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v6 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | cache: 'npm' 32 | 33 | # TODO: use 'ci' 34 | - run: npm i 35 | 36 | - run: npm run build --if-present 37 | 38 | - run: npm test -- __tests__/rdflib.integration.test.js 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oslc-client", 3 | "main": "./index.js", 4 | "description": "OSLC4JS JavaScript OSLC Client", 5 | "type": "module", 6 | "version": "3.0.1", 7 | "author": { 8 | "name": "Jim Amsden", 9 | "email": "jamsden@us.ibm.com" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Neil Davis", 14 | "email": "nbdavis@us.ibm.com" 15 | } 16 | ], 17 | "license": "Apache-2.0", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/OSLC/oslc-client.git" 21 | }, 22 | "homepage": "https://open-services.net/", 23 | "keywords": [ 24 | "oslc", 25 | "ldp", 26 | "domain", 27 | "web", 28 | "rest", 29 | "restful" 30 | ], 31 | "scripts": { 32 | "test": "NODE_OPTIONS=--experimental-vm-modules jest", 33 | "test:watch": "jest --watchAll", 34 | "test:coverage": "jest --coverage" 35 | }, 36 | "jest": { 37 | "testEnvironment": "node", 38 | "collectCoverage": true, 39 | "coverageDirectory": "coverage" 40 | }, 41 | "dependencies": { 42 | "@xmldom/xmldom": "^0.9.8", 43 | "axios": "^1.13.2", 44 | "axios-cookiejar-support": "^6.0.4", 45 | "rdflib": "2.3.2", 46 | "tough-cookie": "^6.0.0" 47 | }, 48 | "devDependencies": { 49 | "jest": "^30.2.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will install Deno then run `deno lint` and `deno test`. 7 | # For more information see: https://github.com/denoland/setup-deno 8 | 9 | name: Deno 10 | 11 | on: 12 | push: 13 | branches: ["master"] 14 | pull_request: 15 | branches: ["master"] 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Setup repo 26 | uses: actions/checkout@v6 27 | 28 | - uses: actions/cache@v5 29 | with: 30 | path: | 31 | ~/.deno 32 | ~/.cache/deno 33 | key: ${{ runner.os }}-deno-${{ hashFiles('**/package.json') }} 34 | 35 | - name: Setup Deno 36 | uses: denoland/setup-deno@v2 37 | with: 38 | deno-version: v2.x 39 | 40 | 41 | - name: Install deps 42 | run: deno install 43 | 44 | # Uncomment this step to verify the use of 'deno fmt' on each commit. 45 | # TODO: enable fmt 46 | # - name: Verify formatting 47 | # run: deno fmt --check 48 | 49 | # TODO: enable lint 50 | # - name: Run linter 51 | # run: deno lint 52 | 53 | # TODO: add tests 54 | # - name: Run tests 55 | # run: deno test -A 56 | -------------------------------------------------------------------------------- /examples/simpleQuery.js: -------------------------------------------------------------------------------- 1 | /** This is simple example that demonstrates how to do simple OSLC queryies 2 | * without having to connect to a server and use a service provider 3 | */ 4 | import OSLCClient from '../OSLCClient.js'; 5 | import { oslc_cm } from '../namespaces.js'; 6 | 7 | // process command line arguments 8 | var args = process.argv.slice(2) 9 | if (args.length != 5) { 10 | console.log("Usage: node simpleQuery.js baseURL projectAreaName workItemID userId password") 11 | process.exit(1) 12 | } 13 | 14 | // setup information 15 | var baseURL = args[0] // required for authentication 16 | var projectArea = args[1] // the queryBase URI from the ServiceProvider 17 | var workItemId = args[2]; // an RTC work item ID (dcterms:identifier) 18 | var userId = args[3] // the user login name 19 | var password = args[4] // User's password 20 | 21 | const client = new OSLCClient(userId, password); 22 | 23 | console.log(`querying: ${workItemId} in ${projectArea}`) 24 | 25 | const sampleQuery = { 26 | prefix: null, 27 | select: 'dcterms:identifier,oslc:shortTitle,dcterms:title', 28 | where: `dcterms:identifier=${workItemId}` 29 | } 30 | 31 | try { 32 | await client.use(baseURL, projectArea, 'CM'); 33 | const resources = await client.queryResources(oslc_cm('ChangeRequest'), sampleQuery); 34 | console.log("✓ Query executed successfully"); 35 | for (let resource of resources) { 36 | console.log(`Resource title: ${resource.getTitle()}`); 37 | } 38 | } catch (error) { 39 | console.error(`✗ Failed to fetch resource: ${error.response?.status} ${error.message}`); 40 | console.error(error.stack); 41 | }; 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/oslcClientExample.js: -------------------------------------------------------------------------------- 1 | import OSLCClient from '../OSLCClient.js'; 2 | 3 | // process command line arguments 4 | var args = process.argv.slice(2) 5 | if (args.length !== 5) { 6 | console.log('Usage: node oslcReauestGet.js baseURI resourceURI projectAreaName userId password') 7 | process.exit(1) 8 | } 9 | 10 | // setup information 11 | var baseURI = args[0]; 12 | var resourceURI = args[1] // the resource to read 13 | var projectArea = args[2]; // Project Area name containing the Work Item/Change Request to be changed 14 | var userId = args[3] // the user login name 15 | var password = args[4] // User's password 16 | 17 | 18 | console.log('Initializing OSLC client...'); 19 | const client = new OSLCClient(userId, password); 20 | 21 | try { 22 | console.log('Setting up the service provider...'); 23 | await client.use(baseURI, projectArea, 'CM'); 24 | console.log('✓ Service provider configured'); 25 | } catch (spError) { 26 | console.error('✗ Failed to setup service provider:'); 27 | console.error(spError.message); 28 | } 29 | 30 | try { 31 | console.log('Fetching sample resource...'); 32 | const resource = await client.getResource(resourceURI); 33 | console.log('✓ Resource retrieved successfully'); 34 | console.log(resource.getTitle()); 35 | } catch (error) { 36 | console.error('✗ Failed to fetch resource:'); 37 | console.error(error.message); 38 | } 39 | 40 | try { 41 | console.log("Executing sample query..."); 42 | const queryResults = await client.query( 43 | 'http://open-services.net/ns/cm#ChangeRequest', 44 | {select:'dcterms:title', 45 | where: 'dcterms:title="SWT Exception"'} 46 | ); 47 | console.log("✓ Query executed successfully"); 48 | console.log(queryResults.serialize()); 49 | } catch (queryError) { 50 | console.error("✗ Query failed:"); 51 | console.error(queryError.message); 52 | if (queryError.response) { 53 | console.error("Status:", queryError.response.status); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /__tests__/OSLCClient.tests.js: -------------------------------------------------------------------------------- 1 | import OSLCClient from "../OSLCClient.js"; 2 | import { oslc, oslc_cm } from "../namespaces.js"; 3 | 4 | var client; 5 | const baseURI = process.env.BASE_URI || "https://elmdemo.smartfacts.com:9443/ccm"; 6 | 7 | // OSLCClient.tests.js 8 | describe("OSLCClient tests", () => { 9 | beforeAll(() => { 10 | /* runs once before all tests */ 11 | const userId = process.env.USER_ID || "admin"; 12 | const password = process.env.PASSWORD || "admin"; 13 | client = new OSLCClient(userId, password); 14 | }); 15 | beforeEach(() => { 16 | /* runs before each test */ 17 | }); 18 | 19 | describe("Test use a service provider", () => { 20 | test("use an ELM project area", async () => { 21 | let projectArea = "JKE Banking CM"; 22 | try { 23 | console.log('baseURI: '+baseURI); 24 | await client.use(baseURI, projectArea, "CM"); 25 | expect(client.sp.getQueryBase(oslc_cm("ChangeRquest"))).toBeDefined(); 26 | expect(client.sp.getCreationFactory(oslc_cm("ChangeRequest"))).toBeDefined(); 27 | } catch (error) { 28 | console.log(`Failed to use project area: ${error.message}`); 29 | expect(error).toBeUndefined(); 30 | } 31 | }, 10000); 32 | }); 33 | 34 | describe("Test CRUD on a Change Request", () => { 35 | // Test cases... 36 | test("Test get a Change Request", async () => { 37 | try { 38 | const changeRequest = await client.getResource( 39 | 'https://elmdemo.smartfacts.com:9443/ccm/resource/itemName/com.ibm.team.workitem.WorkItem/257' 40 | ); 41 | expect(changeRequest).toBeDefined(); 42 | expect(changeRequest.getURI()).toBeDefined(); 43 | expect(changeRequest.getTitle()).toBe("SWT Exception"); 44 | } catch (error) { 45 | console.log(`Failed to get a Change Request: ${error.message}`); 46 | expect(error).toBeUndefined(); 47 | } 48 | }); 49 | }); 50 | 51 | afterEach(() => { 52 | /* cleanup after each test */ 53 | }); 54 | afterAll(() => { 55 | /* final cleanup */ 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /RootServices.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 | import OSLCResource from './OSLCResource.js'; 18 | 19 | /** Encapsulates a Jazz rootservices document on an RDF Store 20 | * 21 | * @constructor 22 | * @param {string} uri - the URI of the Jazz rootservices resource 23 | * @param {Store} store - the RDF Knowledge Base for this rootservices resource 24 | * @param 25 | */ 26 | export default class RootServices extends OSLCResource { 27 | constructor(uri, store, etag=undefined) { 28 | // Store the RDF source in an internal representation for future use 29 | super(uri, store, etag) 30 | } 31 | 32 | /** The RTC rootservices document has a number of jd:oslcCatalogs properties 33 | * that contain inlined oslc:ServiceProviderCatalog instances. 34 | * 35 | * 36 | * 37 | * 38 | * 39 | * We want to get the URI for the CM oslc:domain Service Provider Catalog. 40 | * 41 | * @param {!URI} serviceProviders - the URL of the rootservices. *serviceProviders element 42 | * @returns {string} - the first matching service provider catalog URI 43 | */ 44 | serviceProviderCatalog(serviceProviders) { 45 | var catalog = this.store.the(this.uri, serviceProviders) 46 | return catalog?.uri 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /ServiceProviderCatalog.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 | import OSLCResource from './OSLCResource.js'; 18 | import { dcterms } from './namespaces.js'; 19 | 20 | /** Encapsulates a OSLC ServiceProviderCatalog resource as in-memroy RDF knowledge base 21 | * @class 22 | * 23 | * @constructor 24 | * @param {string} uri - the URI of the OSLC ServiceProviderCatalog resource 25 | * @param {IndexedFormula} store - the RDF Knowledge Base for this service provider catalog 26 | * @param {string} etag - the ETag of the resource 27 | */ 28 | export default class ServiceProviderCatalog extends OSLCResource { 29 | 30 | constructor(uri, store, etag=undefined) { 31 | // Parse the RDF source into an internal representation for future use 32 | super(uri, store, etag) 33 | } 34 | 35 | /** Get the ServiceProvider with the given service provider name. This will also load all the 36 | * services for that service provider so they are available for use. 37 | * 38 | * @param {String} serviceProviderTitle - the dcterms:title of the service provider (e.g., an EWM project area) 39 | * @returns {string} serviceProviderURL - the matching ServiceProvider URL from the service provider catalog 40 | */ 41 | serviceProvider(serviceProviderTitle) { 42 | var sp = this.store.statementsMatching(undefined, dcterms('title'), this.store.literal(serviceProviderTitle 43 | , this.store.sym('http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral'))); 44 | if (!sp) { 45 | return undefined; 46 | } else { 47 | return sp[0].subject.uri 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Compact.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 | import { oslc } from './namespaces.js'; 18 | import OSLCResource from './OSLCResource.js'; 19 | 20 | 21 | /** Implements OSLC Copact resource to support OSLC Resource Preview 22 | * @class 23 | * 24 | * @constructor 25 | * @param {string} uri - the URI of the Jazz rootservices resource 26 | * @param {Store} store - the RDF Knowledge Base for this rootservices resource 27 | */ 28 | export default class Compact extends OSLCResource { 29 | 30 | constructor(uri, store) { 31 | super(uri, store); 32 | } 33 | 34 | getShortTitle() { 35 | return this.get(oslc('shortTitle')); 36 | } 37 | 38 | getIcon() { 39 | return this.get(oslc('icon')); 40 | } 41 | 42 | getIconTitle() { 43 | return this.get(oslc('iconTitle')); 44 | } 45 | 46 | getIconSrcSet() { 47 | return this.get(oslc('iconSrcSet')); 48 | } 49 | 50 | getSmallPreview() { 51 | let preview = this.store.the(this.uri, oslc('smallPreview')); 52 | if (!preview) return null; 53 | let hintHeight = this.store.the(preview, oslc('hintHeight')); 54 | let hintWidth = this.store.the(preview, oslc('hintWidth')); 55 | return { 56 | document: this.store.the(preview, oslc('document')).value, 57 | hintHeight: hintHeight? hintHeight.value: undefined, 58 | hintWidth: hintWidth? hintWidth.value: undefined 59 | } 60 | } 61 | 62 | getLargePreview() { 63 | let preview = this.store.the(this.uri, oslc('largePreview')); 64 | if (!preview) return null; 65 | let hintHeight = this.store.the(preview, oslc('hintHeight')); 66 | let hintWidth = this.store.the(preview, oslc('hintWidth')); 67 | return { 68 | document: this.store.the(preview, oslc('document')).value, 69 | hintHeight: hintHeight? hintHeight.value: undefined, 70 | hintWidth: hintWidth? hintWidth.value: undefined 71 | } 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /namespaces.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright 2014 IBM Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /* Defines some common namespaces, explicitly added to the global scope */ 19 | 20 | import { Namespace, sym } from 'rdflib'; 21 | 22 | export const rdf = Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') 23 | export const rdfs = Namespace('http://www.w3.org/2000/01/rdf-schema#') 24 | export const dcterms = Namespace('http://purl.org/dc/terms/') 25 | export const foaf = Namespace("http://xmlns.com/foaf/0.1/") 26 | export const owl = Namespace("http://www.w3.org/2002/07/owl#") 27 | export const oslc = Namespace('http://open-services.net/ns/core#') 28 | export const oslc_rm = Namespace('http://open-services.net/ns/rm#') 29 | export const oslc_cm = Namespace ('http://open-services.net/ns/cm#') 30 | export const oslc_cm1 = Namespace('http://open-services.net/xmlns/cm/1.0/') 31 | export const rtc_cm = Namespace('http://jazz.net/xmlns/prod/jazz/rtc/cm/1.0/') 32 | export const rtc_cm_ext = Namespace('http://jazz.net/xmlns/prod/jazz/rtc/ext/1.0/') 33 | export const rtc_ext = Namespace('http://jazz.net/xmlns/prod/jazz/rtc/ext/1.0/') 34 | export const rtc_cm_resolvedBy = sym('http://jazz.net/xmlns/prod/jazz/rtc/cm/1.0/com.ibm.team.workitem.linktype.resolvesworkitem.resolvedBy') 35 | export const rtc_cm_relatedArtifact = sym('http://jazz.net/xmlns/prod/jazz/rtc/cm/1.0/com.ibm.team.workitem.linktype.relatedartifact.relatedArtifact') 36 | export const oslc_qm = Namespace('http://open-services.net/ns/qm#') 37 | export const rqm_qm = Namespace('http://jazz.net/ns/qm/rqm#') 38 | export const rqm_process = Namespace('http://jazz.net/xmlns/prod/jazz/rqm/process/1.0/') 39 | export const oslc_qm1 = Namespace("http://open-services.net/xmlns/qm/1.0/") 40 | export const atom = Namespace('http://www.w3.org/2005/Atom') 41 | export const xml = Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') 42 | export const rss = Namespace("http://purl.org/rss/1.0/") 43 | export const xsd = Namespace("http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/#dt-") 44 | export const contact = Namespace("http://www.w3.org/2000/10/swap/pim/contact#") 45 | export const jd = Namespace('http://jazz.net/xmlns/prod/jazz/discovery/1.0/') 46 | 47 | -------------------------------------------------------------------------------- /examples/simpleRMRead.js: -------------------------------------------------------------------------------- 1 | /** This is simple example that demonstrates how to read any OSLC 2 | * resource without having to connect to a server and use a service provider 3 | */ 4 | 'use strict'; 5 | 6 | import OSLCClient from '../OSLCClient.js'; 7 | import { oslc_cm } from '../namespaces.js'; 8 | 9 | // process command line arguments 10 | var args = process.argv.slice(2) 11 | if (args.length !== 3) { 12 | console.log("Usage: node simpleCMRead.js baseURL resourceURI userId password") 13 | process.exit(1) 14 | } 15 | 16 | // setup information 17 | var resourceURI = args[0]; // the resource to read 18 | var userId = args[1] // the user login name 19 | var password = args[2] // User's password 20 | 21 | var client = new OSLCClient(userId, password, 'https://elmdemo.smartfacts.com:9443/gc/configuration/44283'); // there server will be unknown in this case 22 | 23 | console.log(`reading: ${resourceURI}`) 24 | 25 | // Oddly DOORS Next can handle multiple oslc.prefix query parameters, but not a single one with multiple prefixes. 26 | var reqImplementsReqSelectedProps = resourceURI + '?oslc.prefix=dcterms=&oslc.prefix=oslc_cm='; 27 | // You need to manually escape the # in the oslc.prefix URIs 28 | reqImplementsReqSelectedProps = reqImplementsReqSelectedProps + '&oslc.properties=dcterms:title'; 29 | 30 | let result = null; 31 | try { 32 | result = await client.getResource(resourceURI); 33 | console.log(`read resource: ${result.getTitle()}`) 34 | console.log("Resource available link types:") 35 | console.log(result.getLinkTypes()) 36 | console.log(`tracksRequirement: ${result.get('http://open-services.net/ns/config#component')}`) 37 | 38 | // show all the properties: 39 | console.log(`\nAll properties of ${result.getShortTitle()}:`) 40 | let props = result.getProperties() 41 | for (let prop in props) { 42 | console.log(`\t${prop}: ${props[prop]}`) 43 | } 44 | } catch (err) { 45 | console.error(` Could not read ${resourceURI}, got error: ${err}`); 46 | } 47 | 48 | // now read the compact resource representation 49 | try { 50 | result = await client.getCompactResource(resourceURI); 51 | console.log(`read compact resource: ${result.getShortTitle()}, ${result.getTitle()}`) 52 | let smallPreview = result.getSmallPreview(); 53 | console.log(`smallPreview: ${smallPreview.document}, ${smallPreview.hintHeight}, ${smallPreview.hintWidth}`); 54 | } catch (err) { 55 | console.error(` Could not read ${resourceURI}, got error: ${err}`); 56 | } 57 | 58 | // now read using selective properties to get some preview information of the trackedRequirements 59 | try { 60 | result = await client.getResource(reqImplementsReqSelectedProps); 61 | // TODO: selected properties needs additional work 62 | console.log(`\n\nSelected properties of: ${result.getURI()}`) 63 | let props = result.getProperties() 64 | for (let prop in props) { 65 | console.log(`\t${prop}: ${props[prop]}`) 66 | } 67 | } catch (err) { 68 | console.error(` Could not read ${reqImplementsReqSelectedProps}, got error: ${err}`); 69 | } 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /examples/simpleCMRead.js: -------------------------------------------------------------------------------- 1 | /** This is simple example that demonstrates how to read any OSLC 2 | * resource without having to connect to a server and use a service provider 3 | */ 4 | 'use strict'; 5 | 6 | import OSLCClient from '../OSLCClient.js'; 7 | 8 | // process command line arguments 9 | var args = process.argv.slice(2) 10 | if (args.length !== 3) { 11 | console.log("Usage: node simpleCMRead.js baseURL resourceURI userId password") 12 | process.exit(1) 13 | } 14 | 15 | // setup information 16 | var resourceURI = args[0]; // the resource to read 17 | var userId = args[1] // the user login name 18 | var password = args[2] // User's password 19 | 20 | var client = new OSLCClient(userId, password, 'https://elmdemo.smartfacts.com:9443/gc/configuration/44283'); // there server will be unknown in this case 21 | 22 | console.log(`reading: ${resourceURI}`) 23 | 24 | //var reqImplementsReqSelectedProps = resourceURI + '?oslc.prefix=oslc=,oslc_cm=,dcterms='; 25 | var reqImplementsReqSelectedProps = resourceURI + '?oslc.properties=dcterms:title'; 26 | // You need to escape the <> in the oslc.prefix URIs 27 | reqImplementsReqSelectedProps = reqImplementsReqSelectedProps + '&oslc.prefix=oslc_cm=%3Chttp://open-services.net/ns/cm%23%3E,dcterms=%3Chttp://purl.org/dc/terms/%3E'; 28 | 29 | let result = null; 30 | try { 31 | result = await client.getResource(resourceURI); 32 | console.log(`read resource: ${result.getTitle()}`) 33 | console.log("Resource available link types:") 34 | console.log(result.getLinkTypes()) 35 | console.log(`tracksRequirement: ${result.get('http://open-services.net/ns/cm#tracksRequirement')}`) 36 | 37 | // show all the properties: 38 | console.log(`\nAll properties of ${result.getShortTitle()}:`) 39 | let props = result.getProperties() 40 | for (let prop in props) { 41 | console.log(`\t${prop}: ${props[prop]}`) 42 | } 43 | } catch (err) { 44 | console.error(` Could not read ${resourceURI}, got error: ${err}`); 45 | } 46 | 47 | // now read the compact resource representation 48 | try { 49 | result = await client.getCompactResource(resourceURI); 50 | console.log(`read compact resource: ${result.getIdentifier()}, ${result.getShortTitle()}, ${result.getTitle()}`) 51 | let smallPreview = result.getSmallPreview(); 52 | console.log(`smallPreview: ${smallPreview.document}, ${smallPreview.hintHeight}, ${smallPreview.hintWidth}`); 53 | } catch (err) { 54 | console.error(` Could not read ${resourceURI}, got error: ${err}`); 55 | } 56 | 57 | // now read using selective properties to get some preview information of the trackedRequirements 58 | try { 59 | result = await client.getResource(reqImplementsReqSelectedProps); 60 | // TODO: selected properties needs additional work 61 | console.log(`\n\nSelected properties of: ${result.getURI()}`) 62 | let props = result.getProperties() 63 | for (let prop in props) { 64 | console.log(`\t${prop}: ${props[prop]}`) 65 | } 66 | } catch (err) { 67 | console.error(` Could not read ${reqImplementsReqSelectedProps}, got error: ${err}`); 68 | } 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oslc-client 2 | 3 | [![npm](https://img.shields.io/npm/v/oslc-client)](https://www.npmjs.com/package/oslc-client) 4 | [![Discourse status](https://img.shields.io/discourse/https/meta.discourse.org/status.svg)](https://forum.open-services.net/) 5 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/OSLC/chat) 6 | 7 | An OSLC client API Node.js module 8 | 9 | oslc-client is a JavaScript Node.js module supporting OSLC client and server development. The client API exposes the OSLC 10 | core and domain capabilities through a simple JavaScript API on the OSLC REST services. 11 | 12 | oslc-client exploits the dynamic and asynchronous capabilities of JavaScript and Node.js to build and API that can easily 13 | adapt to any OSLC domain, extensions to domains, and/or integrations between domains. 14 | 15 | This version updates previous 2.x.x versions to use axios for HTTP access and modern JavaScript async/await to handle 16 | asynchronous operations. 17 | 18 | Version 3.0.1 is a maintenance release that alows oslc-client run in the browser or in a node.js environment. 19 | 20 | * 21 | 22 | ## Usage 23 | 24 | To use oslc-client, include a dependency in your OSLC client app's package.json file: 25 | 26 | ``` 27 | "dependencies": { 28 | "oslc-client": "~3.0.0", 29 | } 30 | ``` 31 | * Servers are identified by a server root URL that is typically https://host:port/domain. For example, https://acme.com/ccm would be the server URL for an instance of RTC. 32 | * Servers provide a rootservices resource at their server root URL that can be used to discover the discovery services provided by the server. This typically provides the URLs to the service provider catalogs and TRS providers. For example https://acme.com/ccm/rootservices provides this information for an instance of RTC. By convention, access to the rootservices resource does not require authentication. This is to provide the OAuth URLs often needed to do authentication. 33 | * Authentication is done through extensions to axios request interceptors that automatically use jazz FORM based authentication by POSTing user credentials to serverURI/j_security_check in response to an authentication challenge indicated by header x-com-ibm-team-repository-web-auth-msg=authrequired 34 | * Resources are often identified by their dcterms:identifier property, and a readById function is provided to conveniently query resources by ID. 35 | 36 | # examples 37 | 38 | See examples/updateCR.js for an example client application that connects to a server, uses a particular service provider, queries, creates, reads, updates, and deletes ChangeRequest resources managed by RTC. 39 | 40 | ## Contributors 41 | 42 | Contributors: 43 | 44 | * Jim Amsden (IBM) 45 | 46 | ## License 47 | 48 | Licensed under the Apache License, Version 2.0 (the "License"); 49 | you may not use this file except in compliance with the License. 50 | You may obtain a copy of the License at 51 | 52 | http://www.apache.org/licenses/LICENSE-2.0 53 | 54 | Unless required by applicable law or agreed to in writing, software 55 | distributed under the License is distributed on an "AS IS" BASIS, 56 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 57 | See the License for the specific language governing permissions and 58 | limitations under the License. 59 | 60 | -------------------------------------------------------------------------------- /ServiceProvider.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 | import OSLCResource from './OSLCResource.js'; 20 | import {oslc} from './namespaces.js'; 21 | 22 | 23 | /** Encapsulates a OSLC ServiceProvider resource as in-memroy RDF knowledge base 24 | * This is an asynchronous constructor. The callback is called when the ServiceProvider 25 | * has discovered all its services 26 | * @class 27 | * @constructor 28 | * @param {!URI} uri - the URI of the ServiceProvider 29 | * @param {request} request - for making HTTP requests 30 | * @param etag - the ETag of the resource 31 | */ 32 | export default class ServiceProvider extends OSLCResource { 33 | constructor(uri, store, etag=undefined) { 34 | // Parse the RDF source into an internal representation for future use 35 | super(uri, store, etag) 36 | } 37 | 38 | /* 39 | * Get the queryBase URL for an OSLC QueryCapability with the given oslc:resourceType 40 | * 41 | * @param {Symbol} a symbol for the desired oslc:resourceType 42 | * @returns {string} the queryBase URL used to query resources of that type 43 | */ 44 | getQueryBase(resourceType) { 45 | let resourceTypeSym = (typeof resourceType === 'string')? this.store.sym(resourceType): resourceType; 46 | let services = this.store.each(this.uri, oslc('service')); 47 | for (let service of services) { 48 | var queryCapabilities = this.store.each(service, oslc('queryCapability')); 49 | for (let queryCapability of queryCapabilities) { 50 | if (this.store.statementsMatching(queryCapability, oslc('resourceType'), resourceTypeSym).length) { 51 | return this.store.the(queryCapability, oslc('queryBase')).value 52 | } 53 | } 54 | } 55 | return null 56 | } 57 | 58 | 59 | /* 60 | * Get the creation URL for an OSLC CreationFactory with the given oslc:resourceType 61 | * 62 | * @param {Symbol | string} a symbol for, or the name of the desired oslc:resourceType 63 | * @returns {string} the creation URL used to create resources of that type 64 | */ 65 | getCreationFactory(resourceType) { 66 | var services = this.store.each(this.uri, oslc('service')) 67 | for (var service in services) { 68 | var creationFactories = this.store.each(services[service], oslc('creationFactory')); 69 | for (var creationFactory in creationFactories) { 70 | if (typeof(resourceType) === 'string') { 71 | var types = this.store.each(creationFactories[creationFactory], oslc('resourceType')) 72 | for (var aType in types) { 73 | if (types[aType].uri.endsWith(resourceType)) return this.store.the(creationFactories[creationFactory], oslc('creation')).uri 74 | } 75 | } else if (this.store.statementsMatching(creationFactories[creationFactory], oslc('resourceType'), resourceType).length === 1) { 76 | return this.store.the(creationFactories[creationFactory], oslc('creation')).uri 77 | } 78 | } 79 | } 80 | return null 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /examples/updateCR.js: -------------------------------------------------------------------------------- 1 | /** This is the same example as update CR.js, but using async/await 2 | * A simple example OSLC client application that demonstrates how to utilize 3 | * typical OSLC integration capabilities for doing CRUD operations on resource. 4 | * The example is based on the OSLC Workshop example at: 5 | */ 6 | 'use strict'; 7 | 8 | import OSLCClient from '../OSLCClient.js'; 9 | import OSLCResource from '../OSLCResource.js'; 10 | import { oslc_cm, rdf, dcterms } from '../namespaces.js'; 11 | 12 | 13 | var args = process.argv.slice(2); 14 | if (args.length != 5) { 15 | console.log("Usage: node updateCR.js serverURI projectArea workItemId userId password"); 16 | process.exit(1); 17 | } 18 | 19 | // setup information - server, user, project area, work item to update 20 | var serverURI = args[0]; // Public URI of an RTC server 21 | var serviceProvider = args[1]; // Project Area name containing the Work Item/Change Request to be changed 22 | var changeRequestID = args[2]; // Work Item/Change Request id to change 23 | var userId = args[3]; // the user login name 24 | var password = args[4]; // User's password 25 | 26 | var client = new OSLCClient(userId, password); 27 | 28 | // Connect to the OSLC server, use a service provider container, and do some 29 | // operations on resources. All operations are asynchronous but often have 30 | // to be done in a specific order. This example use async to control the order 31 | 32 | console.log(`Creating, updating and deleting a ChangeRequest in ${serviceProvider}...`); 33 | 34 | var changeRequest = null // the change request we'll be manipulating 35 | var results = null // the results of OSLCClient request 36 | 37 | // use the service provider (a project area in this case) 38 | await client.use(serverURI, serviceProvider); 39 | 40 | // delete a resource if it exists (possibly from a previous run) 41 | try { 42 | results = await client.queryResources(oslc_cm('ChangeRequest'), {where: 'dcterms:title="deleteMe"'}); 43 | if (results?.length > 0) { 44 | // found a resource with tigle deleteMe, delete the resource 45 | // there may be more than one, but we'll only delete one 46 | let resource = results[0]; 47 | console.log(`deleting: ${resource.getURI()}`) 48 | try { 49 | results = await client.deleteResource(resource) 50 | console.log('deleted resource deleteMe') 51 | } catch (err) { 52 | console.error('Could not delete resource: '+err); 53 | } 54 | } else { 55 | console.log('resource "deleteMe" not found') 56 | } 57 | } catch (err) { 58 | console.error("Cannot find resource deleteMe: ", err); 59 | } 60 | 61 | // create a resource (this is what will be deleted on the next run) 62 | var deleteMe = new OSLCResource(); 63 | deleteMe.setTitle('deleteMe'); 64 | deleteMe.setDescription('A test resource to delete'); 65 | deleteMe.set(rdf('type'), oslc_cm('ChangeRequest')); 66 | try { 67 | results = await client.createResource('task', deleteMe); 68 | console.log('Created: ' + results.getURI()); 69 | } catch (err) { 70 | console.error('Could not create resource: '+err); 71 | } 72 | 73 | // read an existing ChangeRequest resource by identifier 74 | try { 75 | changeRequest = await client.queryResources(oslc_cm('ChangeRequest'), {select:'*', where:`dcterms:identifier="${changeRequestID}"`}); 76 | if (changeRequest?.length > 0) { 77 | changeRequest = changeRequest[0]; 78 | console.log('Got Change Request: '+changeRequest.getURI()); 79 | console.log(changeRequest.get(dcterms('title'))); 80 | } else { 81 | console.log('No Change Request found with identifier: '+changeRequestID); 82 | process.exit(1); 83 | } 84 | 85 | } catch (err) { 86 | console.error('Could not read resource: '+err); 87 | } 88 | 89 | // update the ChangeRequest just read 90 | // Just add the current date to the end of the description 91 | var description = changeRequest.get(dcterms('description')) + " - " + new Date(); 92 | changeRequest.set(dcterms('description'), description); 93 | console.log('Updated resource description: '+changeRequest.getDescription()); 94 | try { 95 | results = await client.putResource(changeRequest); 96 | } catch (err) { 97 | console.error('Could not update resource: '+err); 98 | } 99 | 100 | // all done 101 | console.log('Done'); 102 | -------------------------------------------------------------------------------- /OSLCResource.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 | import * as $rdf from 'rdflib'; 18 | import { dcterms, oslc } from './namespaces.js'; 19 | 20 | 21 | /** This is a generic OSLC resource. Properties for 22 | * a particular domain resource will be added dynamically 23 | * when it is read. This allows OSLCResource to be used 24 | * on any domain without change or extension. 25 | * 26 | * However, subclasses may be created for any OSLC domain 27 | * as a convenience for those domain resources. 28 | * 29 | * OSLCResource is a class wrapper on an rdflib Store. 30 | * Some common OSLC properties are accessed directly through 31 | * accessor methods. Other properties are accessed through the 32 | * get and set property methods through reflection. 33 | 34 | * @author Jim Amsden 35 | * @class 36 | * @parm {string} uri - the URI sym of this resource 37 | * @param {Store} kb - the Knowledge Base that contains the resource RDF graph 38 | */ 39 | export default class OSLCResource { 40 | constructor(uri=null, store=null, etag=null) { 41 | if (uri) { 42 | this.queryURI = uri; 43 | const resourceURI = new URL(uri); 44 | this.uri = $rdf.sym(resourceURI.origin + resourceURI.pathname); 45 | this.store = store; 46 | this.etag = etag; 47 | } else { 48 | // construct an empty resource 49 | this.uri = $rdf.blankNode(); 50 | this.store = $rdf.graph(); 51 | this.etag = undefined; 52 | } 53 | } 54 | 55 | getURI() { 56 | return this.uri.value; 57 | } 58 | 59 | /** 60 | * Get a property of the resource. This method assumes any property could 61 | * be multi-valued or undefined. Based on open-world assumptions, it is not 62 | * considered an error to attempt to get a property that doesn't exist. This 63 | * would simply return undefined. 64 | * 65 | * @param {string|symbol} property - the RDF property to get 66 | * @returns - undefined, single object URL or literal value, or array of values 67 | */ 68 | get(property) { 69 | let p = typeof property === 'string' ? this.store.sym(property) : property; 70 | let result = this.store.each(this.uri, p); 71 | if (result.length === 1) { 72 | return result[0].value; 73 | } else if (result.length > 1) { 74 | return result.map((v) => v.value); 75 | } else { 76 | return undefined; 77 | } 78 | } 79 | 80 | /** 81 | * The following accessor functions are for common OSLC core vocabulary 82 | * that most OSLC resources are likely to have. Subclasses for OSLC domain 83 | * vocabularies would likely add additional accessor methods for the 84 | * properties defined in their domain specification. 85 | */ 86 | 87 | /** 88 | * Get the resource dcterms:identifier 89 | * 90 | * @returns {string} - dcterms:identifier value 91 | */ 92 | getIdentifier() { 93 | return this.get(dcterms('identifier')); 94 | } 95 | 96 | /** 97 | * Get the resource dcterms:title 98 | * 99 | * @returns {string} - dcterms:title value(s) 100 | */ 101 | getTitle() { 102 | var result = this.get(dcterms('title')); 103 | return Array.isArray(result) ? result[0] : result; 104 | } 105 | 106 | getShortTitle() { 107 | return this.get(oslc('shortTitle')); 108 | } 109 | 110 | /** 111 | * Get the resource dcterms:description 112 | * 113 | * @returns {string} - dcterms:description value 114 | */ 115 | getDescription() { 116 | var result = this.get(dcterms('description')); 117 | return Array.isArray(result) ? result[0] : result; 118 | } 119 | 120 | /** 121 | * Set the resource dcterms:title 122 | * 123 | * @param {string} value - dcterms:title value 124 | */ 125 | setTitle(value) { 126 | this.set(dcterms('title'), $rdf.literal(value)); 127 | } 128 | 129 | /** 130 | * Set the resource dcterms:description 131 | * 132 | * @param {string} value - dcterms:description value 133 | */ 134 | setDescription(value) { 135 | this.set(dcterms('description'), $rdf.literal(value)); 136 | } 137 | 138 | /** 139 | * Set a property of the resource. This method assumes any property could 140 | * be multi-valued or undefined. Based on open-world assumptions, it is not 141 | * considered an error to attempt to set a property that doesn't exist. So 142 | * set can be used to add new properties. Using undefined for the value will 143 | * remove the property. 144 | * 145 | * If the property is multi-valued, the caller should include all the desired 146 | * values since the property will be completely replaced with the new value. 147 | * 148 | * @param {string} property - the RDF property to set 149 | * @param {Node} value - the new value, all old values will be removed 150 | * @returns {void} 151 | */ 152 | set(property, value) { 153 | // first remove the current values 154 | let p = typeof property === 'string' ? this.store.sym(property) : property; 155 | var subject = this.uri; 156 | this.store.remove(this.store.statementsMatching(subject, p, undefined)); 157 | if (typeof value == 'undefined') return; 158 | if (Array.isArray(value)) { 159 | for (var i = 0; i < value.length; i++) { 160 | this.store.add(subject, p, value[i]); 161 | } 162 | } else { 163 | this.store.add(subject, p, value); 164 | } 165 | } 166 | 167 | /** 168 | * Return an Set of link types (i.e. ObjectProperties) provided by this resource 169 | */ 170 | getLinkTypes() { 171 | let linkTypes = new Set(); 172 | let statements = this.store.statementsMatching(this.uri, undefined, undefined); 173 | for (let statement of statements) { 174 | if (statement.object instanceof $rdf.NamedNode) 175 | linkTypes.add(statement.predicate.value); 176 | } 177 | return linkTypes; 178 | } 179 | 180 | /** 181 | * Return an Array of name-value pairs for all properties of by this resource 182 | */ 183 | getProperties() { 184 | let result = {}; 185 | let statements = this.store.statementsMatching(this.uri, undefined, undefined); 186 | for (let statement of statements) { 187 | if (result[statement.predicate.value] != null) { 188 | if (!(result[statement.predicate.value] instanceof Array)) { 189 | result[statement.predicate.value] = [ 190 | result[statement.predicate.value], 191 | ]; 192 | } 193 | result[statement.predicate.value].push(statement.object.value); 194 | } else { 195 | result[statement.predicate.value] = statement.object.value; 196 | } 197 | } 198 | return result; 199 | } 200 | } 201 | 202 | 203 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /__tests__/rdflib.integration.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Integration tests for rdflib functionality 3 | * These tests verify that rdflib operations work correctly with the OSLC client 4 | */ 5 | 6 | import * as $rdf from 'rdflib'; 7 | import { Namespace, sym } from 'rdflib'; 8 | import OSLCResource from '../OSLCResource.js'; 9 | import { dcterms, oslc, rdf } from '../namespaces.js'; 10 | 11 | describe('rdflib Integration Tests', () => { 12 | describe('Basic rdflib operations', () => { 13 | test('should create an empty graph', () => { 14 | const graph = $rdf.graph(); 15 | expect(graph).toBeDefined(); 16 | expect(graph.statements).toBeDefined(); 17 | expect(graph.statements.length).toBe(0); 18 | }); 19 | 20 | test('should create named nodes with sym', () => { 21 | const node = sym('http://example.com/resource'); 22 | expect(node).toBeDefined(); 23 | expect(node.value).toBe('http://example.com/resource'); 24 | }); 25 | 26 | test('should create literals', () => { 27 | const literal = $rdf.literal('test value'); 28 | expect(literal).toBeDefined(); 29 | expect(literal.value).toBe('test value'); 30 | }); 31 | 32 | test('should create blank nodes', () => { 33 | const blankNode = $rdf.blankNode(); 34 | expect(blankNode).toBeDefined(); 35 | expect(blankNode.value).toMatch(/^n[0-9]+$/); 36 | }); 37 | 38 | test('should create namespaces', () => { 39 | const testNs = Namespace('http://example.com/ns#'); 40 | expect(testNs).toBeDefined(); 41 | const term = testNs('term'); 42 | expect(term).toBeDefined(); 43 | expect(term.value).toBe('http://example.com/ns#term'); 44 | }); 45 | 46 | test('should check for NamedNode instance', () => { 47 | const namedNode = sym('http://example.com/resource'); 48 | expect(namedNode instanceof $rdf.NamedNode).toBe(true); 49 | }); 50 | }); 51 | 52 | describe('Graph operations', () => { 53 | test('should add statements to graph', () => { 54 | const graph = $rdf.graph(); 55 | const subject = sym('http://example.com/resource'); 56 | const predicate = dcterms('title'); 57 | const object = $rdf.literal('Test Title'); 58 | 59 | graph.add(subject, predicate, object); 60 | 61 | expect(graph.statements.length).toBe(1); 62 | expect(graph.statements[0].subject.value).toBe('http://example.com/resource'); 63 | expect(graph.statements[0].object.value).toBe('Test Title'); 64 | }); 65 | 66 | test('should query statements from graph', () => { 67 | const graph = $rdf.graph(); 68 | const subject = sym('http://example.com/resource'); 69 | const predicate = dcterms('title'); 70 | const object = $rdf.literal('Test Title'); 71 | 72 | graph.add(subject, predicate, object); 73 | 74 | const results = graph.each(subject, predicate); 75 | expect(results).toBeDefined(); 76 | expect(results.length).toBe(1); 77 | expect(results[0].value).toBe('Test Title'); 78 | }); 79 | 80 | test('should match statements in graph', () => { 81 | const graph = $rdf.graph(); 82 | const subject = sym('http://example.com/resource'); 83 | const predicate = dcterms('title'); 84 | const object = $rdf.literal('Test Title'); 85 | 86 | graph.add(subject, predicate, object); 87 | 88 | const statements = graph.statementsMatching(subject, predicate, undefined); 89 | expect(statements).toBeDefined(); 90 | expect(statements.length).toBe(1); 91 | expect(statements[0].object.value).toBe('Test Title'); 92 | }); 93 | 94 | test('should remove statements from graph', () => { 95 | const graph = $rdf.graph(); 96 | const subject = sym('http://example.com/resource'); 97 | const predicate = dcterms('title'); 98 | const object = $rdf.literal('Test Title'); 99 | 100 | graph.add(subject, predicate, object); 101 | expect(graph.statements.length).toBe(1); 102 | 103 | const statements = graph.statementsMatching(subject, predicate, undefined); 104 | graph.remove(statements); 105 | 106 | expect(graph.statements.length).toBe(0); 107 | }); 108 | 109 | test('should handle multiple values for same property', () => { 110 | const graph = $rdf.graph(); 111 | const subject = sym('http://example.com/resource'); 112 | const predicate = dcterms('creator'); 113 | 114 | graph.add(subject, predicate, $rdf.literal('Creator 1')); 115 | graph.add(subject, predicate, $rdf.literal('Creator 2')); 116 | 117 | const results = graph.each(subject, predicate); 118 | expect(results.length).toBe(2); 119 | expect(results.map(r => r.value)).toContain('Creator 1'); 120 | expect(results.map(r => r.value)).toContain('Creator 2'); 121 | }); 122 | }); 123 | 124 | describe('RDF parsing', () => { 125 | test('should parse RDF/XML', () => { 126 | const rdfXml = ` 127 | 129 | 130 | Test Resource 131 | Test Description 132 | 133 | `; 134 | 135 | const graph = $rdf.graph(); 136 | $rdf.parse(rdfXml, graph, 'http://example.com/', 'application/rdf+xml'); 137 | 138 | const subject = sym('http://example.com/resource1'); 139 | const title = graph.any(subject, dcterms('title')); 140 | 141 | expect(title).toBeDefined(); 142 | expect(title.value).toBe('Test Resource'); 143 | }); 144 | 145 | test('should parse Turtle', () => { 146 | const turtle = `@prefix dcterms: . 147 | @prefix : . 148 | 149 | :resource1 dcterms:title "Test Resource" ; 150 | dcterms:description "Test Description" .`; 151 | 152 | const graph = $rdf.graph(); 153 | $rdf.parse(turtle, graph, 'http://example.com/', 'text/turtle'); 154 | 155 | const subject = sym('http://example.com/resource1'); 156 | const title = graph.any(subject, dcterms('title')); 157 | 158 | expect(title).toBeDefined(); 159 | expect(title.value).toBe('Test Resource'); 160 | }); 161 | 162 | test('should handle parsing errors gracefully', () => { 163 | const invalidRdf = 'This is not valid RDF'; 164 | const graph = $rdf.graph(); 165 | 166 | expect(() => { 167 | $rdf.parse(invalidRdf, graph, 'http://example.com/', 'application/rdf+xml'); 168 | }).toThrow(); 169 | }); 170 | }); 171 | 172 | describe('RDF serialization', () => { 173 | test('should serialize to RDF/XML', () => { 174 | const graph = $rdf.graph(); 175 | const subject = sym('http://example.com/resource1'); 176 | 177 | graph.add(subject, dcterms('title'), $rdf.literal('Test Resource')); 178 | graph.add(subject, dcterms('description'), $rdf.literal('Test Description')); 179 | 180 | const serialized = graph.serialize(null, 'application/rdf+xml'); 181 | 182 | expect(serialized).toBeDefined(); 183 | expect(typeof serialized).toBe('string'); 184 | expect(serialized).toContain('Test Resource'); 185 | expect(serialized).toContain('Test Description'); 186 | expect(serialized).toContain('http://example.com/resource1'); 187 | }); 188 | 189 | test('should serialize to Turtle', () => { 190 | const graph = $rdf.graph(); 191 | const subject = sym('http://example.com/resource1'); 192 | 193 | graph.add(subject, dcterms('title'), $rdf.literal('Test Resource')); 194 | 195 | const serialized = graph.serialize(null, 'text/turtle'); 196 | 197 | expect(serialized).toBeDefined(); 198 | expect(typeof serialized).toBe('string'); 199 | expect(serialized).toContain('Test Resource'); 200 | }); 201 | }); 202 | 203 | describe('OSLCResource with rdflib', () => { 204 | test('should create OSLCResource with empty graph', () => { 205 | const resource = new OSLCResource(); 206 | 207 | expect(resource).toBeDefined(); 208 | expect(resource.store).toBeDefined(); 209 | expect(resource.uri).toBeDefined(); 210 | }); 211 | 212 | test('should create OSLCResource with URI', () => { 213 | const graph = $rdf.graph(); 214 | const uri = 'http://example.com/resource1'; 215 | const resource = new OSLCResource(uri, graph); 216 | 217 | expect(resource.getURI()).toBe(uri); 218 | expect(resource.store).toBe(graph); 219 | }); 220 | 221 | test('should set and get resource title', () => { 222 | const resource = new OSLCResource(); 223 | 224 | resource.setTitle('Test Title'); 225 | const title = resource.getTitle(); 226 | 227 | expect(title).toBe('Test Title'); 228 | }); 229 | 230 | test('should set and get resource description', () => { 231 | const resource = new OSLCResource(); 232 | 233 | resource.setDescription('Test Description'); 234 | const description = resource.getDescription(); 235 | 236 | expect(description).toBe('Test Description'); 237 | }); 238 | 239 | test('should get properties from resource', () => { 240 | const graph = $rdf.graph(); 241 | const uri = 'http://example.com/resource1'; 242 | const subject = sym(uri); 243 | 244 | graph.add(subject, dcterms('title'), $rdf.literal('Test Title')); 245 | graph.add(subject, dcterms('identifier'), $rdf.literal('ID123')); 246 | 247 | const resource = new OSLCResource(uri, graph); 248 | const properties = resource.getProperties(); 249 | 250 | expect(properties).toBeDefined(); 251 | expect(properties['http://purl.org/dc/terms/title']).toBe('Test Title'); 252 | expect(properties['http://purl.org/dc/terms/identifier']).toBe('ID123'); 253 | }); 254 | 255 | test('should get link types from resource', () => { 256 | const graph = $rdf.graph(); 257 | const uri = 'http://example.com/resource1'; 258 | const subject = sym(uri); 259 | 260 | // Add a literal property (should not be in link types) 261 | graph.add(subject, dcterms('title'), $rdf.literal('Test Title')); 262 | 263 | // Add a link property (should be in link types) 264 | graph.add(subject, dcterms('creator'), sym('http://example.com/user1')); 265 | graph.add(subject, oslc('serviceProvider'), sym('http://example.com/sp')); 266 | 267 | const resource = new OSLCResource(uri, graph); 268 | const linkTypes = resource.getLinkTypes(); 269 | 270 | expect(linkTypes).toBeDefined(); 271 | expect(linkTypes.size).toBe(2); 272 | expect(linkTypes.has('http://purl.org/dc/terms/creator')).toBe(true); 273 | expect(linkTypes.has('http://open-services.net/ns/core#serviceProvider')).toBe(true); 274 | expect(linkTypes.has('http://purl.org/dc/terms/title')).toBe(false); 275 | }); 276 | 277 | test('should handle multi-valued properties', () => { 278 | const graph = $rdf.graph(); 279 | const uri = 'http://example.com/resource1'; 280 | const subject = sym(uri); 281 | 282 | graph.add(subject, dcterms('creator'), $rdf.literal('Creator 1')); 283 | graph.add(subject, dcterms('creator'), $rdf.literal('Creator 2')); 284 | 285 | const resource = new OSLCResource(uri, graph); 286 | const creators = resource.get(dcterms('creator')); 287 | 288 | expect(Array.isArray(creators)).toBe(true); 289 | expect(creators.length).toBe(2); 290 | expect(creators).toContain('Creator 1'); 291 | expect(creators).toContain('Creator 2'); 292 | }); 293 | 294 | test('should set property and remove old values', () => { 295 | const graph = $rdf.graph(); 296 | const uri = 'http://example.com/resource1'; 297 | const subject = sym(uri); 298 | 299 | graph.add(subject, dcterms('title'), $rdf.literal('Old Title')); 300 | 301 | const resource = new OSLCResource(uri, graph); 302 | resource.set(dcterms('title'), $rdf.literal('New Title')); 303 | 304 | const title = resource.get(dcterms('title')); 305 | expect(title).toBe('New Title'); 306 | 307 | // Verify old value is removed 308 | const allTitles = graph.each(subject, dcterms('title')); 309 | expect(allTitles.length).toBe(1); 310 | }); 311 | }); 312 | 313 | describe('Namespace operations', () => { 314 | test('should create terms from imported namespaces', () => { 315 | const titleProp = dcterms('title'); 316 | expect(titleProp).toBeDefined(); 317 | expect(titleProp.value).toBe('http://purl.org/dc/terms/title'); 318 | 319 | const serviceProp = oslc('serviceProvider'); 320 | expect(serviceProp).toBeDefined(); 321 | expect(serviceProp.value).toBe('http://open-services.net/ns/core#serviceProvider'); 322 | }); 323 | 324 | test('should create type from rdf namespace', () => { 325 | const typeProperty = rdf('type'); 326 | expect(typeProperty).toBeDefined(); 327 | expect(typeProperty.value).toBe('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'); 328 | }); 329 | }); 330 | 331 | describe('Round-trip parsing and serialization', () => { 332 | test('should parse and serialize RDF/XML maintaining data integrity', () => { 333 | const rdfXml = ` 334 | 336 | 337 | Test Resource 338 | ID123 339 | 340 | `; 341 | 342 | // Parse 343 | const graph = $rdf.graph(); 344 | $rdf.parse(rdfXml, graph, 'http://example.com/', 'application/rdf+xml'); 345 | 346 | // Verify data was parsed 347 | const subject = sym('http://example.com/resource1'); 348 | const title = graph.any(subject, dcterms('title')); 349 | const identifier = graph.any(subject, dcterms('identifier')); 350 | 351 | expect(title.value).toBe('Test Resource'); 352 | expect(identifier.value).toBe('ID123'); 353 | 354 | // Serialize 355 | const serialized = graph.serialize(null, 'application/rdf+xml'); 356 | 357 | // Parse again to verify 358 | const graph2 = $rdf.graph(); 359 | $rdf.parse(serialized, graph2, 'http://example.com/', 'application/rdf+xml'); 360 | 361 | const title2 = graph2.any(subject, dcterms('title')); 362 | const identifier2 = graph2.any(subject, dcterms('identifier')); 363 | 364 | expect(title2.value).toBe('Test Resource'); 365 | expect(identifier2.value).toBe('ID123'); 366 | }); 367 | }); 368 | }); 369 | -------------------------------------------------------------------------------- /OSLCClient.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { sym } from 'rdflib'; 3 | import * as $rdf from "rdflib"; 4 | import { rdfs, oslc, oslc_cm1, oslc_rm, oslc_qm1 } from './namespaces.js'; 5 | import OSLCResource from './OSLCResource.js'; 6 | import Compact from './Compact.js'; 7 | import RootServices from './RootServices.js'; 8 | import ServiceProviderCatalog from './ServiceProviderCatalog.js'; 9 | import ServiceProvider from './ServiceProvider.js'; 10 | 11 | // Conditional imports for Node.js only 12 | let wrapper, CookieJar, DOMParser; 13 | const isNodeEnvironment = typeof window === 'undefined'; 14 | 15 | if (isNodeEnvironment) { 16 | // Node.js imports 17 | const cookiejarSupport = await import('axios-cookiejar-support'); 18 | wrapper = cookiejarSupport.wrapper; 19 | const toughCookie = await import('tough-cookie'); 20 | CookieJar = toughCookie.CookieJar; 21 | const xmldom = await import('@xmldom/xmldom'); 22 | DOMParser = xmldom.DOMParser; 23 | } else { 24 | // Browser: use native DOMParser 25 | DOMParser = window.DOMParser; 26 | } 27 | 28 | // Service providers properties 29 | const serviceProviders = { 30 | 'CM': oslc_cm1('cmServiceProviders'), 31 | 'RM': oslc_rm('rmServiceProviders'), 32 | 'QM': oslc_qm1('qmServiceProviders') 33 | }; 34 | 35 | 36 | 37 | /** 38 | * An OSLCClient provides a simple interface to access OSLC resources 39 | * and perform operations like querying, creating, and updating resources. 40 | * It handles authentication, service provider discovery, and resource management. 41 | */ 42 | export default class OSLCClient { 43 | constructor(user, password, configuration_context = null) { 44 | this.userid = user; 45 | this.password = password; 46 | this.configuration_context = configuration_context; 47 | this.rootservices = null; 48 | this.spc = null; 49 | this.sp = null; 50 | this.ownerMap = new Map(); 51 | this.isNodeEnvironment = isNodeEnvironment; 52 | 53 | if (isNodeEnvironment) { 54 | this.jar = new CookieJar(); 55 | } 56 | 57 | // Create a base configuration 58 | const baseConfig = { 59 | timeout: 30000, 60 | headers: { 61 | 'Accept': 'application/rdf+xml, text/turtle;q=0.9, application/ld+json;q=0.8, application/json;q=0.7, application/xml;q=0.6, text/xml;q=0.5, */*;q=0.1', 62 | 'OSLC-Core-Version': '2.0' 63 | }, 64 | validateStatus: status => status === 401 || status < 400 // Accept all 2xx responses 65 | }; 66 | 67 | // Configure for Node.js or Browser 68 | if (isNodeEnvironment) { 69 | // Node.js: use a cookie jar and keep-alive 70 | baseConfig.keepAlive = true; 71 | baseConfig.jar = this.jar; 72 | this.client = wrapper(axios.create(baseConfig)); 73 | } else { 74 | // Browser: use withCredentials for automatic cookie handling 75 | baseConfig.withCredentials = true; 76 | this.client = axios.create(baseConfig); 77 | } 78 | 79 | // Add the Configuration-Context header if one is given 80 | if (configuration_context) { 81 | this.client.defaults.headers.common['Configuration-Context'] = configuration_context; 82 | } 83 | 84 | // Response interceptor for handling auth challenges 85 | this.client.interceptors.response.use( 86 | async response => { 87 | const originalRequest = response.config; 88 | const wwwAuthenticate = response?.headers?.['www-authenticate']; 89 | 90 | // Check if this is a JEE Forms authentication challenge 91 | if (response?.headers['x-com-ibm-team-repository-web-auth-msg'] === 'authrequired') { 92 | try { 93 | // Perform the login (JEE form auth typically uses j_username and j_password) 94 | let url = new URL(response.config.url); 95 | const paths = url.pathname.split('/'); 96 | url.pathname = paths[1] ? `/${paths[1]}/j_security_check` : '/j_security_check'; 97 | 98 | // In browser, form-based auth may require a backend proxy due to CORS 99 | if (!isNodeEnvironment) { 100 | console.warn('Form-based authentication in browser requires CORS-enabled backend or proxy'); 101 | } 102 | 103 | response = await this.client.post(url.toString(), 104 | new URLSearchParams({ 105 | 'j_username': this.userid, 106 | 'j_password': this.password 107 | }).toString(), 108 | { 109 | headers: { 110 | 'Content-Type': 'application/x-www-form-urlencoded' 111 | }, 112 | maxRedirects: 0, 113 | validateStatus: (status) => status === 302 // for successful login 114 | } 115 | ); 116 | // After successful login, retry the original request with updated cookies 117 | response = await this.client.request(originalRequest); 118 | return response; 119 | } catch (error) { 120 | console.error('Error during JEE authentication:', error.message); 121 | return Promise.reject(error); 122 | } 123 | } else if (response.status === 401 && wwwAuthenticate?.includes('jauth realm')) { 124 | const token_uri = wwwAuthenticate.match(/token_uri="([^"]+)"/)[1]; 125 | try { 126 | // Refresh the token using the provided token_uri 127 | const tokenResponse = await this.client.post(token_uri, 128 | new URLSearchParams({ 129 | username: this.userid, 130 | password: this.password, 131 | }).toString(), 132 | { 133 | headers: { 134 | 'Content-Type': 'application/x-www-form-urlencoded', 135 | 'Accept': 'text/plain', 136 | } 137 | } 138 | ); 139 | // retry the original request with the new token 140 | const newToken = tokenResponse.data; // Refresh token 141 | originalRequest.headers['Authorization'] = `Bearer ${newToken}`; 142 | return await this.client.request(originalRequest); // Retry the request 143 | } catch (error) { 144 | console.error('Error during jauth realm authentication:', error.message); 145 | return Promise.reject(error); 146 | } 147 | } else if (response.status === 401) { 148 | // Retry with basic authentication for e.g., Jazz Authorization Server 149 | originalRequest.auth = { 150 | username: this.userid, 151 | password: this.password 152 | }; 153 | return await this.client.request(originalRequest); 154 | } else { 155 | // No authentication challenge, proceed with the response 156 | return response; 157 | } 158 | }, 159 | ); 160 | }; 161 | 162 | /** 163 | * Set the OSLCClient to use a given service provider of the given domain, 164 | * 165 | * @param {*} serviceProviderName 166 | * @param {*} domain 167 | */ 168 | async use(server_url, serviceProviderName, domain = 'CM') { 169 | this.base_url = server_url?.endsWith('/') ? server_url.slice(0, -1) : server_url; 170 | 171 | // Read the server's rootservices document 172 | let resource; 173 | // Fetch the rootservices document, this is an unprotected resource 174 | try { 175 | resource = await this.getResource(`${this.base_url}/rootservices`); 176 | this.rootservices = new RootServices(resource.getURI(), resource.store, resource.etag); 177 | } catch (error) { 178 | console.error('Error fetching rootservices:', error); 179 | throw new Error('Failed to fetch rootservices document'); 180 | } 181 | 182 | // Get ServiceProviderCatalog URL from the rootservices resource 183 | const spcURL = this.rootservices.serviceProviderCatalog(serviceProviders[domain]); 184 | if (!spcURL) { 185 | throw new Error(`No ServiceProviderCatalog for ${domain} services`); 186 | } 187 | try { 188 | resource = await this.getResource(spcURL); 189 | this.spc = new ServiceProviderCatalog(resource.getURI(), resource.store, resource.etag); 190 | } catch (error) { 191 | console.error('Error fetching ServiceProviderCatalog:', error); 192 | } 193 | 194 | // Lookup the the serviceProviderName in the ServiceProviderCatalog 195 | let spURL = this.spc.serviceProvider(serviceProviderName); 196 | if (!spURL) { 197 | throw new Error(`${serviceProviderName} not found in service catalog`); 198 | } 199 | resource = await this.getResource(spURL); 200 | this.sp = new ServiceProvider(resource.getURI(), resource.store, resource.etag); 201 | } 202 | 203 | /** 204 | * 205 | * @param {*} url The URL of the resource 206 | * @param {*} oslc_version OSLC version to use, defaults to 2.0 207 | * @param {*} accept The Accept header value, defaults to 'application/rdf+xml' 208 | * @returns an OSLCResource object containing the resource data 209 | */ 210 | async getResource(url, oslc_version = '2.0', accept = 'application/rdf+xml') { 211 | const headers = { 212 | 'Accept': accept, 213 | 'OSLC-Core-Version': oslc_version 214 | }; 215 | 216 | let response 217 | try { 218 | response = await this.client.get(url, { headers }); 219 | } catch (error) { 220 | console.error('Error fetching resource:', error); 221 | throw error; 222 | } 223 | const etag = response.headers.etag; 224 | const contentType = response.headers['content-type']; 225 | 226 | // This only handles the headers that are used in the OSLC spec 227 | if (contentType.includes('text/xml') || contentType.includes('application/xml')) { 228 | return { etag, xml: new DOMParser().parseFromString(response.data) }; 229 | } else if (contentType.includes('application/atom+xml')) { 230 | return { etag, feed: response.data }; 231 | } else { 232 | // assume the content-type is some RDF representation 233 | // Create a new graph for this resource 234 | const graph = $rdf.graph(); 235 | try { 236 | $rdf.parse(response.data, graph, url, contentType) 237 | } catch (err) { 238 | console.error(err) 239 | } 240 | return new OSLCResource(url, graph, etag); 241 | } 242 | throw new Error(`Unsupported content type: ${contentType}`); 243 | } 244 | 245 | 246 | /** 247 | * 248 | * @param {*} url The URL of the resource 249 | * @param {*} oslc_version OSLC version to use, defaults to 2.0 250 | * @param {*} accept The Accept header value, defaults to 'application/rdf+xml' 251 | * @returns an OSLCResource object containing the resource data 252 | */ 253 | async getCompactResource(url, oslc_version = '2.0', accept = 'application/x-oslc-compact+xml') { 254 | const headers = { 255 | 'Accept': accept, 256 | 'OSLC-Core-Version': oslc_version 257 | }; 258 | 259 | let response 260 | try { 261 | response = await this.client.get(url, { headers }); 262 | } catch (error) { 263 | console.error('Error fetching Compact resource:', error); 264 | throw error; 265 | } 266 | const etag = response.headers.etag; 267 | const contentType = response.headers['content-type']; 268 | 269 | // Create a new graph for this resource 270 | const graph = $rdf.graph(); 271 | // contentType is application/x-oslc-compact+xml, but that is RDF/XML specific to OSLC Compact 272 | $rdf.parse(response.data, graph, url, 'application/rdf+xml'); 273 | return new Compact(url, graph, etag); 274 | } 275 | 276 | 277 | async putResource(resource, eTag = null, oslc_version = '2.0') { 278 | const graph = resource.store; 279 | if (!graph) { 280 | throw new Error('Resource has no data to update'); 281 | } 282 | const url = resource.getURI(); 283 | const headers = { 284 | 'OSLC-Core-Version': oslc_version, 285 | 'Content-Type': 'application/rdf+xml; charset=utf-8', 286 | 'Accept': 'application/rdf+xml' 287 | }; 288 | if (eTag) { 289 | headers['If-Match'] = eTag; 290 | } 291 | const body = graph.serialize(null, 'application/rdf+xml'); 292 | const response = await this.client.put(url, body, { headers }); 293 | 294 | if (response.status !== 200 && response.status !== 201) { 295 | throw new Error( 296 | `Failed to update resource ${url}. Status: ${response.status}\n${response.data}` 297 | ); 298 | } 299 | return resource; 300 | } 301 | 302 | async createResource(resourceType, resource, oslc_version = '2.0') { 303 | const graph = resource.store; 304 | if (!graph) { 305 | throw new Error('Resource has no data to create'); 306 | } 307 | const creationFactory = this.sp.getCreationFactory(resourceType); 308 | if (!creationFactory) { 309 | throw new Error(`No creation factory found for ${resourceType}`); 310 | } 311 | const headers = { 312 | 'Content-Type': 'application/rdf+xml; charset=utf-8', 313 | 'Accept': 'application/rdf+xml; charset=utf-8', 314 | 'OSLC-Core-Version': oslc_version 315 | }; 316 | 317 | const body = graph.serialize(null, 'application/rdf+xml'); 318 | let response = null; 319 | try { 320 | response = await this.client.post(creationFactory, body, { headers }); 321 | if (response.status !== 200 && response.status !== 201) { 322 | throw new Error(`Failed to create resource. Status: ${response.status}\n${response.data}`); 323 | } 324 | } catch (error) { 325 | console.error('Error creating resource:', error); 326 | throw error; 327 | } 328 | const location = response.headers.location; 329 | resource = await this.getResource(location); 330 | return resource; 331 | } 332 | 333 | async deleteResource(resource, oslc_version = '2.0') { 334 | const graph = resource.store; 335 | if (!graph) { 336 | throw new Error('Resource has no data to delete'); 337 | } 338 | const url = resource.getURI(); 339 | const headers = { 340 | 'Accept': 'application/rdf+xml; charset=utf-8', 341 | 'OSLC-Core-Version': oslc_version, 342 | 'X-Jazz-CSRF-Prevent': '1' 343 | }; 344 | 345 | // In Node.js, try to get JSESSIONID from cookie jar 346 | if (isNodeEnvironment && this.jar) { 347 | try { 348 | const cookies = this.jar.getCookiesSync(url); 349 | const sessionCookie = cookies.find(cookie => cookie.key === 'JSESSIONID'); 350 | if (sessionCookie) { 351 | headers['X-Jazz-CSRF-Prevent'] = sessionCookie.value; 352 | } 353 | } catch (error) { 354 | // If cookie retrieval fails, continue with default value 355 | console.debug('Could not retrieve JSESSIONID from cookie jar:', error.message); 356 | } 357 | } 358 | 359 | try { 360 | const response = await this.client.delete(url, { headers }); 361 | if (response.status !== 200 && response.status !== 204) { 362 | throw new Error(`Failed to delete resource. Status: ${response.status}\n${response.data}`); 363 | } 364 | } catch (error) { 365 | console.error('Error deleting resource:', error); 366 | throw error; 367 | } 368 | return undefined; 369 | } 370 | 371 | async queryResources(resourceType, query) { 372 | const kb = await this.query(resourceType, query); 373 | // create an OSLCResource for each result member 374 | // TODO: getting the members must use the discovered member predicate, rdfs:member is the default 375 | let resources = []; 376 | let members = kb.statementsMatching(null, rdfs('member'), null); 377 | for (let member of members) { 378 | let memberStatements = kb.statementsMatching(member.object, undefined, undefined); 379 | let memberKb = $rdf.graph(); 380 | memberKb.add(memberStatements); 381 | resources.push(new OSLCResource(member.object.value, memberKb)); 382 | } 383 | return resources; 384 | } 385 | 386 | async query(resourceType, query) { 387 | const queryBase = this.sp.getQueryBase(resourceType); 388 | if (!queryBase) { 389 | throw new Error(`No query capability found for ${resourceType}`); 390 | } 391 | return this.queryWithBase(queryBase, query); 392 | } 393 | 394 | async queryWithBase(queryBase, query) { 395 | const headers = { 396 | 'OSLC-Core-Version': '2.0', 397 | 'Accept': 'application/rdf+xml', 398 | 'X-Jazz-CSRF-Prevent': '1' 399 | }; 400 | 401 | const params = new URLSearchParams(); 402 | if (query?.prefix) params.append('oslc.prefix', query.prefix); 403 | if (query?.select) params.append('oslc.select', query.select); 404 | if (query?.where) params.append('oslc.where', query.where); 405 | if (query?.orderBy) params.append('oslc.orderBy', query.orderBy); 406 | params.append('oslc.paging', 'false'); 407 | 408 | let url = `${queryBase}?${params.toString()}`; 409 | let response = await this.client.get(url, { headers }); 410 | if (response.status !== 200) { 411 | throw new Error(`Failed to query resource. Status: ${response.status}\n${response.data}`); 412 | } 413 | const contentType = response.headers['content-type']; 414 | const store = $rdf.graph(); 415 | try { 416 | $rdf.parse(response.data, store, url, contentType) 417 | } catch (err) { 418 | console.error(err) 419 | } 420 | let nextPage = store.any(sym(queryBase), oslc('nextPage'), null)?.value; 421 | while (nextPage) { 422 | response = await this.client.get(nextPage, { headers }); 423 | try { 424 | $rdf.parse(response.data, store, url, contentType) 425 | } catch (err) { 426 | console.error(err) 427 | } 428 | nextPage = store.any(sym(nextPage), oslc('nextPage'), null)?.value; 429 | } 430 | 431 | return store; 432 | } 433 | 434 | async getOwner(url) { 435 | if (this.ownerMap.has(url)) { 436 | return this.ownerMap.get(url); 437 | } 438 | 439 | const headers = { 'Accept': 'application/rdf+xml' }; 440 | const response = await this.client.get(url, { headers }); 441 | 442 | if (response.status !== 200) { 443 | return 'Unknown'; 444 | } 445 | const contentLocation = response.headers['content-location'] || url; 446 | const contentType = response.headers['content-type']; 447 | const store = $rdf.graph(); 448 | try { 449 | $rdf.parse(response.data, store, url, contentType) 450 | } catch (err) { 451 | console.error(err) 452 | } 453 | const name = store.any( 454 | sym(contentLocation), 455 | sym('http://xmlns.com/foaf/0.1/name'), 456 | null 457 | )?.value; 458 | 459 | if (name) { 460 | this.ownerMap.set(url, name); 461 | return name; 462 | } 463 | return 'Unknown'; 464 | } 465 | 466 | async getQueryBase(resourceType) { 467 | const query = ` 468 | PREFIX oslc: ${oslc()} 469 | SELECT ?qb WHERE { 470 | ?sp oslc:service ?s . 471 | ?s oslc:queryCapability ?qc . 472 | ?qc oslc:resourceType <${resourceType}> . 473 | ?qc oslc:queryBase ?qb . 474 | }`; 475 | 476 | const results = this.sp.store.querySync(query); 477 | if (!results?.length) { 478 | throw new Error(`No query capability found for ${resourceType}`); 479 | } 480 | return results[0].qb.value; 481 | } 482 | 483 | async getCreationFactory(resourceType) { 484 | const query = ` 485 | PREFIX oslc: <${oslc().uri}> 486 | SELECT ?cfurl WHERE { 487 | ?sp oslc:service ?s . 488 | ?s oslc:creationFactory ?cf . 489 | ?cf oslc:usage <${resourceType}> . 490 | ?cf oslc:creation ?cfurl . 491 | }`; 492 | 493 | const results = await this.sp.store.sparqlQuery(query); 494 | if (!results?.length) { 495 | throw new Error(`No creation factory found for ${resourceType}`); 496 | } 497 | return results[0].cfurl.value; 498 | } 499 | } --------------------------------------------------------------------------------