├── .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 | [](https://www.npmjs.com/package/oslc-client)
4 | [](https://forum.open-services.net/)
5 | [](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 | }
--------------------------------------------------------------------------------