Commit b968b1f3 authored by Laurent Wouters's avatar Laurent Wouters
Browse files

Implementing fetchable views

parent 082a9cb26e14
......@@ -22,11 +22,11 @@
import { RdfStore, Triple, Term, NamedNode, BlankNode, Literal } from "rdflib";
import * as React from "react";
import {
ViewImplementation,
ViewRegistrySourceInline,
ViewRegistrySourceKind,
ViewLocationKind
} from "./view-registry";
import { ViewImplementation } from "./view-impl";
import { $rdf, getValueOf } from "./rdf";
import { ViewRenderer } from "./application";
......@@ -118,10 +118,7 @@ function renderRdfNode(node: Term): React.ReactNode {
* @param store The RDF store
* @param primaryTopic The primary topic
*/
export function hasTraitHasPrimary(
store: RdfStore,
primaryTopic: string
): boolean {
function hasTraitHasPrimary(store: RdfStore, primaryTopic: string): boolean {
return primaryTopic != null;
}
......@@ -131,10 +128,7 @@ export function hasTraitHasPrimary(
* @param store The RDF store
* @param primaryTopic The primary topic
*/
export function hasTraitDbPediaPerson(
store: RdfStore,
primaryTopic: string
): boolean {
function hasTraitDbPediaPerson(store: RdfStore, primaryTopic: string): boolean {
if (!hasTraitHasPrimary(store, primaryTopic)) return false;
let a = store.each(
$rdf.sym(primaryTopic),
......@@ -191,34 +185,6 @@ export class DBPediaPersonRendering implements ViewImplementation {
}
}
/**
* The source for the default views
*/
export const DEFAULTS_SOURCE: ViewRegistrySourceInline = {
name: "Source of default views",
kind: ViewRegistrySourceKind.Inlined,
descriptors: [
{
identifier: "::Defaults::Triples",
name: "RDF Triples",
description: "Renders a complete RDF dataset using the N-Triples syntax",
entrypoint: "VIEW_DEFAULTS_TRIPLES",
location: {
kind: ViewLocationKind.Embedded
}
},
{
identifier: "::Defaults::DbPediaPerson",
name: "DbPedia Person",
description: "Renders a person from DbPedia",
entrypoint: "VIEW_DEFAULTS_DBPEDIA_PERSON",
location: {
kind: ViewLocationKind.Embedded
}
}
]
};
/**
* The implementation for the Rdf triples view
*/
......
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*
* CubicWeb is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 2.1 of the License, or (at your option)
* any later version.
*
* CubicWeb is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import {
ViewRegistrySourceInline,
ViewRegistrySourceKind,
ViewLocationKind
} from "./view-registry";
/**
* The source for the default views
*/
export const DEFAULTS_SOURCE: ViewRegistrySourceInline = {
name: "Source of default views",
kind: ViewRegistrySourceKind.Inlined,
descriptors: [
{
identifier: "::Defaults::Triples",
name: "RDF Triples",
description: "Renders a complete RDF dataset using the N-Triples syntax",
entrypoint: "VIEW_DEFAULTS_TRIPLES",
location: {
kind: ViewLocationKind.Embedded
}
},
{
identifier: "::Defaults::DbPediaPerson",
name: "DbPedia Person",
description: "Renders a person from DbPedia",
entrypoint: "VIEW_DEFAULTS_DBPEDIA_PERSON",
location: {
kind: ViewLocationKind.Embedded
}
}
]
};
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*
* CubicWeb is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 2.1 of the License, or (at your option)
* any later version.
*
* CubicWeb is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
/// <reference path="./rdflib-interface.d.ts"/>
import { RdfStore } from "rdflib";
import { ReactNode } from "react";
import { ViewRenderer, Application } from "./application";
import {
ViewDescriptor,
ViewLocationKind,
ViewLocationRemote,
ViewRegistry
} from "./view-registry";
/**
* The implementation of a view
*/
export interface ViewImplementation {
/**
* Gets the view's priority (greater number indicate greater priority)
* @param store The RDF store holding the data to be rendered
* @param target The target resource to be rendered
*/
priorityFor(store: RdfStore, target: string): number;
/**
* Renders a resource belonging to an RDF store
* @param renderer The parent renderer
* @param store The RDF store holding the data to be rendered
* @param root The root resource
* @param target The target resource for this view
*/
render(
renderer: ViewRenderer,
store: RdfStore,
root: string,
target: string
): ReactNode;
}
/**
* Fetches the implementation corresponding to a descriptor
* @param descriptor The descriptor
*/
function fetchImplementation(
descriptor: ViewDescriptor
): Promise<ViewImplementation> {
if (descriptor.location.kind == ViewLocationKind.Embedded) {
return new Promise((resolve, reject) => {
let result = eval(descriptor.entrypoint);
if (result == null) {
reject("No implementation");
}
resolve(result);
});
} else if (descriptor.location.kind == ViewLocationKind.Remote) {
return new Promise((resolve, reject) => {
if (document != null && document != undefined) {
// we are in the context of a document
var scriptTag = document.createElement("script");
scriptTag.setAttribute(
"src",
(descriptor.location as ViewLocationRemote).uri
);
scriptTag.addEventListener(
"load",
(event: Event): any => {
let result = eval(descriptor.entrypoint);
if (result == null) {
reject("No implementation");
}
resolve(result);
},
false
);
document.head.appendChild(scriptTag);
} else {
let xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status < 200 || xmlHttp.status >= 300) {
reject("HTTP error: " + xmlHttp.status);
}
if (xmlHttp.responseText.length == 0) {
reject("Empty result");
}
let evaluation = eval(xmlHttp.responseText);
let result = eval(descriptor.entrypoint);
if (result == null) {
reject("No implementation");
}
resolve(result);
}
};
xmlHttp.open(
"GET",
(descriptor.location as ViewLocationRemote).uri,
true
);
xmlHttp.setRequestHeader("Accept", "application/javascript");
xmlHttp.send();
}
});
}
}
/**
* A repository of view implementations
*/
interface ViewImplementations {
[id: string]: ViewImplementation;
}
/**
* Loads all implementations for the registered views
* @param registry The registry to load
*/
function loadImplementations(
registry: ViewRegistry
): Promise<ViewImplementations> {
return new Promise<ViewImplementations>(
(
resolve: (impls: ViewImplementations) => void,
reject: (reason: any) => void
) => {
var finished = 0;
var target = Object.keys(registry.descriptors).length;
var implementations: ViewImplementations = {};
let onPartFinished: () => void = () => {
finished += 1;
if (finished >= target) resolve(implementations);
};
Object.keys(registry.descriptors).map((id: string) => {
let descriptor = registry.descriptors[id];
fetchImplementation(descriptor)
.then((implementation: ViewImplementation) => {
implementations[id] = implementation;
onPartFinished();
})
.catch((reason: string) => {
console.log(
"Failed to fetch implementation for view " +
descriptor.name +
": " +
reason
);
onPartFinished();
});
});
}
);
}
/**
* A prioritized view implementation
*/
interface PrioritizedViewImplementation {
/**
* The related implementation
*/
implementation: ViewImplementation;
/**
* The associated priority
*/
priority: number;
}
/**
* Resoles a view for an RDF datastore and a resource in it
* @param implementations The view implementations to use
* @param store The RDF store holding the data to be rendered
* @param target The target RDF resource to render
*/
function resolveViewFor(
implementations: ViewImplementations,
store: RdfStore,
target: string
): ViewImplementation {
let sorted: PrioritizedViewImplementation[] = Object.keys(implementations)
.map((id: string) => {
let impl = implementations[id];
return {
implementation: implementations[id],
priority: impl.priorityFor(store, target)
};
})
.filter((a: PrioritizedViewImplementation) => a.priority >= 0)
.sort(
(x: PrioritizedViewImplementation, y: PrioritizedViewImplementation) =>
x.priority - y.priority
);
if (sorted.length == 0) return null;
return sorted[0].implementation;
}
/**
* A renderer for an application
*/
class ViewRendererImpl implements ViewRenderer {
/**
* Initializes a renderer
* @param application The parent application
* @param registry The registry to use
* @param implementations The view implementations
*/
constructor(
application: Application,
registry: ViewRegistry,
implementations: ViewImplementations
) {
this.application = application;
this.registry = registry;
this.implementations = implementations;
}
/**
* The parent application
*/
application: Application;
/**
* The registry to use
*/
registry: ViewRegistry;
/**
* The view implementations
*/
implementations: ViewImplementations;
/**
* Renders a resource belonging to an RDF store
* @param store The RDF store holding the data to be rendered
* @param root The root resource
* @param target The target resource for this view
*/
render(store: RdfStore, root: string, target: string = root): ReactNode {
let view = resolveViewFor(this.implementations, store, target);
if (view == null) return null;
return view.render(this, store, root, target);
}
}
/**
* Creates a new view renderer
* @param application The current application
* @param registry The view registry to use
*/
export function newRenderer(
application: Application,
registry: ViewRegistry
): Promise<ViewRenderer> {
return new Promise<ViewRenderer>(
(
resolve: (renderer: ViewRenderer) => void,
reject: (reason: any) => void
) => {
loadImplementations(registry).then((impls: ViewImplementations) => {
let renderer = new ViewRendererImpl(application, registry, impls);
resolve(renderer);
});
}
);
}
......@@ -18,11 +18,6 @@
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
/// <reference path="./rdflib-interface.d.ts"/>
import { RdfStore } from "rdflib";
import { ReactNode } from "react";
import { ViewRenderer, Application } from "./application";
/**
* The kind of locations for the location of a view definition
*/
......@@ -175,111 +170,6 @@ export function fetchDescriptors(
}
}
/**
* The implementation of a view
*/
export interface ViewImplementation {
/**
* Gets the view's priority (greater number indicate greater priority)
* @param store The RDF store holding the data to be rendered
* @param target The target resource to be rendered
*/
priorityFor(store: RdfStore, target: string): number;
/**
* Renders a resource belonging to an RDF store
* @param renderer The parent renderer
* @param store The RDF store holding the data to be rendered
* @param root The root resource
* @param target The target resource for this view
*/
render(
renderer: ViewRenderer,
store: RdfStore,
root: string,
target: string
): ReactNode;
}
/**
* Fetches the implementation corresponding to a descriptor
* @param descriptor The descriptor
*/
export function fetchImplementation(
descriptor: ViewDescriptor
): Promise<ViewImplementation> {
if (descriptor.location.kind == ViewLocationKind.Embedded) {
return new Promise((resolve, reject) => {
let result = eval(descriptor.entrypoint);
if (result == null) {
reject("No implementation");
}
resolve(result);
});
} else if (descriptor.location.kind == ViewLocationKind.Remote) {
return new Promise((resolve, reject) => {
if (document != null && document != undefined) {
// we are in the context of a document
var scriptTag = document.createElement("script");
scriptTag.setAttribute(
"src",
(descriptor.location as ViewLocationRemote).uri
);
scriptTag.addEventListener(
"load",
(event: Event): any => {
let result = eval(descriptor.entrypoint);
if (result == null) {
reject("No implementation");
}
resolve(result);
},
false
);
document.head.appendChild(scriptTag);
} else {
let xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status < 200 || xmlHttp.status >= 300) {
reject("HTTP error: " + xmlHttp.status);
}
if (xmlHttp.responseText.length == 0) {
reject("Empty result");
}
let evaluation = eval(xmlHttp.responseText);
let result = eval(descriptor.entrypoint);
if (result == null) {
reject("No implementation");
}
resolve(result);
}
};
xmlHttp.open(
"GET",
(descriptor.location as ViewLocationRemote).uri,
true
);
xmlHttp.setRequestHeader("Accept", "application/javascript");
xmlHttp.send();
}
});
}
}
/**
* A prioritized view implementation
*/
interface PrioritizedViewImplementation {
/**
* The related implementation
*/
implementation: ViewImplementation;
/**
* The associated priority
*/
priority: number;
}
/**
* A registry of views
*/
......@@ -290,7 +180,6 @@ export class ViewRegistry {
constructor() {
this.sources = [];
this.descriptors = {};
this.implementations = {};
}
/**
......@@ -301,10 +190,6 @@ export class ViewRegistry {
* The registry of descriptors
*/
descriptors: { [id: string]: ViewDescriptor };
/**
* The implementations
*/
implementations: { [id: string]: ViewImplementation };
}
/**
......@@ -330,94 +215,3 @@ export function loadDescriptors(registry: ViewRegistry) {
});
});
}
/**
* Loads all implementations for the registered views
* @param registry The registry to load
*/
export function loadImplementations(registry: ViewRegistry) {
registry.implementations = {};
Object.keys(registry.descriptors).map((id: string) => {
let descriptor = registry.descriptors[id];
fetchImplementation(descriptor)
.catch((reason: string) => {
console.log(
"Failed to fetch implementation for view " +
descriptor.name +
": " +
reason
);
})
.then((implementation: ViewImplementation) => {
registry.implementations[id] = implementation;
});
});
}
/**
* Resoles a view for an RDF datastore and a resource in it
* @param registry The registry to use
* @param store The RDF store holding the data to be rendered
* @param target The target RDF resource to render
*/
export function resolveViewFor(
registry: ViewRegistry,
store: RdfStore,
target: string
): ViewImplementation {
let implementations = registry.implementations;
let sorted: PrioritizedViewImplementation[] = Object.keys(
registry.implementations
)
.map((id: string) => {
let impl = implementations[id];
return {
implementation: implementations[id],
priority: impl.priorityFor(store, target)
};
})
.filter((a: PrioritizedViewImplementation) => a.priority >= 0)
.sort(
(x: PrioritizedViewImplementation, y: PrioritizedViewImplementation) =>
x.priority - y.priority
);
if (sorted.length == 0) return null;
return sorted[0].implementation;
}
/**
* A renderer for an application
*/
export class ViewRendererImpl implements ViewRenderer {
/**
* Initializes a renderer
* @param application The parent application
* @param registry The registry to use
*/
constructor(application: Application, registry: ViewRegistry) {
this.application = application;
this.registry = registry;
loadImplementations(registry);
}
/**
* The parent application