From 0a637ee67b68e1098cbecf6234f061c18c2a1c69 Mon Sep 17 00:00:00 2001 From: Patrizio Bekerle <patrizio.bekerle@tugraz.at> Date: Tue, 16 Jul 2019 13:30:56 +0200 Subject: [PATCH] Initial commit with JSONLD class and basic utils.js --- packages/common/jsonld.js | 221 +++++++++++++++++++++++++++++++++++ packages/common/package.json | 4 + packages/common/utils.js | 44 +++++++ 3 files changed, 269 insertions(+) create mode 100644 packages/common/jsonld.js create mode 100644 packages/common/package.json create mode 100644 packages/common/utils.js diff --git a/packages/common/jsonld.js b/packages/common/jsonld.js new file mode 100644 index 00000000..ad80f9ff --- /dev/null +++ b/packages/common/jsonld.js @@ -0,0 +1,221 @@ +"use strict"; + +import utils from "./utils"; + +let instances = {}; +let successFunctions = {}; +let failureFunctions = {}; +let initStarted = {}; + +// "module.exports = class JSONLD" doesn't work with rollup because of above "import" +export default class JSONLD { + constructor(baseApiUrl, entities) { + this.entities = entities; + this.baseApiUrl = baseApiUrl; + + let idToEntityNameMatchList = {}; + for (const entityName in entities) { + const id = entities[entityName]["@id"]; + idToEntityNameMatchList[id] = entityName; + } + + this.idToEntityNameMatchList = idToEntityNameMatchList; + } + + static initialize(apiUrl, successFnc, failureFnc) { + // if init api call was already successfully finished execute the success function + if (instances[apiUrl] !== undefined) { + if (typeof successFnc == 'function') successFnc(instances[apiUrl]); + + return; + } + + // init the arrays + if (successFunctions[apiUrl] === undefined) successFunctions[apiUrl] = []; + if (failureFunctions[apiUrl] === undefined) failureFunctions[apiUrl] = []; + + // add success and failure functions + if (typeof successFnc == 'function') successFunctions[apiUrl].push(successFnc); + if (typeof failureFnc == 'function') failureFunctions[apiUrl].push(failureFnc); + + // check if api call was already started + if (initStarted[apiUrl] !== undefined) { + return; + } + + initStarted[apiUrl] = true; + + // window.VPUAuthToken will be set by on vpu-auth-init + document.addEventListener("vpu-auth-init", function(e) + { + const xhr = new XMLHttpRequest(); + xhr.open("GET", apiUrl, true); + xhr.setRequestHeader('Authorization', 'Bearer ' + window.VPUAuthToken); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + const json = JSON.parse(xhr.responseText); + + let entryPoints = {}; + for (let property in json) { + // for some reason the properties start with a lower case character + if (!property.startsWith("@")) entryPoints[property.toLowerCase()] = json[property]; + } + + // read the link header of the api response +// const utils = require("./utils"); + const links = utils.parseLinkHeader(this.getResponseHeader("link")); + + // get the hydra apiDocumentation url + const apiDocUrl = links["http://www.w3.org/ns/hydra/core#apiDocumentation"]; + + if (apiDocUrl !== undefined) { + // load the hydra apiDocumentation + const docXhr = new XMLHttpRequest(); + docXhr.open("GET", apiDocUrl, true); + docXhr.setRequestHeader("Content-Type", "application/json"); + docXhr.onreadystatechange = function () { + if (docXhr.readyState === 4 && docXhr.status === 200) { + const json = JSON.parse(docXhr.responseText); + const supportedClasses = json["hydra:supportedClass"]; + + let entities = {}; + const baseUrl = utils.parseBaseUrl(apiUrl); + + // gather the entities + supportedClasses.forEach(function (classData) { + // add entry point url + const entityName = classData["hydra:title"]; + let entryPoint = entryPoints[entityName.toLowerCase()]; + if (entryPoint !== undefined && !entryPoint.startsWith("http")) entryPoint = baseUrl + entryPoint; + classData["@entryPoint"] = entryPoint; + + entities[entityName] = classData; + }); + + const instance = new JSONLD(baseUrl, entities); + instances[apiUrl] = instance; + + // return the initialized JSONLD object + for (const fnc of successFunctions[apiUrl]) if (typeof fnc == 'function') fnc(instance); + successFunctions[apiUrl] = []; + } else { + for (const fnc of failureFunctions[apiUrl]) if (typeof fnc == 'function') fnc(); + failureFunctions[apiUrl] = []; + } + }; + + docXhr.send(); + } else { + for (const fnc of failureFunctions[apiUrl]) if (typeof fnc == 'function') fnc(); + failureFunctions[apiUrl] = []; + } + } else { + for (const fnc of failureFunctions[apiUrl]) if (typeof fnc == 'function') fnc(); + failureFunctions[apiUrl] = []; + } + }; + + xhr.send(); + }); + } + + static getInstance(apiUrl) { + return instances[apiUrl]; + } + + getEntityForIdentifier(identifier) { + let entityName = this.getEntityNameForIdentifier(identifier); + return this.getEntityForEntityName(entityName); + } + + getEntityForEntityName(entityName) { + return this.entities[entityName]; + } + + getApiUrlForIdentifier(identifier) { + return this.getEntityForIdentifier(identifier)["@entryPoint"]; + } + + getApiUrlForEntityName(entityName) { + return this.getEntityForEntityName(entityName)["@entryPoint"]; + } + + getEntityNameForIdentifier(identifier) { + return this.idToEntityNameMatchList[identifier]; + } + + getApiIdentifierList() { + let keys = []; + for (const property in this.idToEntityNameMatchList) { + keys.push(property); + } + + return keys; + } + + /** + * Expands a member of a list to a object with schema.org properties + * + * @param member + */ + expandMember(member) { + const type = member["@type"]; + + const entity = this.getEntityForIdentifier(type); + let result = {"@id": member["@id"]}; + + entity["hydra:supportedProperty"].forEach(function (property) { + const id = property["hydra:property"]["@id"]; + const title = property["hydra:title"]; + + result[id] = member[title]; + }); + + return result; + } + + /** + * Compacts an expanded member of a list to a object with local properties + * + * @param member + * @param localContext + */ + static compactMember(member, localContext) { + let result = {}; + + for (const property in localContext) { + const value = member[localContext[property]]; + + if (value !== undefined) { + result[property] = value; + } + } + + return result; + } + + /** + * Transforms hydra members to a local context + * + * @param data + * @param localContext + * @returns {Array} + */ + transformMembers(data, localContext) { + const members = data['hydra:member']; + + if (members === undefined || members.length === 0) { + return []; + } + + let results = []; + let that = this; + + members.forEach(function (member) { + results.push(JSONLD.compactMember(that.expandMember(member), localContext)); + }); + + return results; + } +}; diff --git a/packages/common/package.json b/packages/common/package.json new file mode 100644 index 00000000..845728d5 --- /dev/null +++ b/packages/common/package.json @@ -0,0 +1,4 @@ +{ + "name": "vpu-common", + "version": "1.0.1" +} diff --git a/packages/common/utils.js b/packages/common/utils.js new file mode 100644 index 00000000..4773eded --- /dev/null +++ b/packages/common/utils.js @@ -0,0 +1,44 @@ +module.exports = { + /** + * Parses a link header + * + * The node module parse-link-header didn't work, so https://gist.github.com/niallo/3109252 became handy + * + * @param header + */ + parseLinkHeader: (header) => { + if (header.length === 0) { + throw new Error("input must not be of zero length"); + } + + // Split parts by comma + const parts = header.split(','); + const links = {}; + + // Parse each part into a named link + for(let i=0; i<parts.length; i++) { + const section = parts[i].split(';'); + if (section.length !== 2) { + throw new Error("section could not be split on ';'"); + } + const url = section[0].replace(/<(.*)>/, '$1').trim(); + const name = section[1].replace(/rel="(.*)"/, '$1').trim(); + links[name] = url; + } + + return links; + }, + + /** + * Parses the base url from an url + * + * @param url + * @returns {string} + */ + parseBaseUrl: (url) => { + const pathArray = url.split('/'); + const protocol = pathArray[0]; + const host = pathArray[2]; + return protocol + '//' + host; + }, +}; -- GitLab