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

Implementing fetchable views

parent 82d83e96ec90
/* This is the BuckleScript configuration file. Note that this is a comment;
BuckleScript comes with a JSON parser that supports comments and trailing
comma. If this screws with your editor highlighting, please tell us by filing
an issue! */
{
"name": "react-template",
"reason": {
"react-jsx": 2
},
"sources": {
"dir" : "src",
"subdirs" : true
},
"package-specs": [{
"module": "commonjs",
"in-source": true
}],
"suffix": ".bs.js",
"namespace": true,
"bs-dependencies": [
"reason-react",
"bs-fetch",
"bs-json"
],
"refmt": 3
}
......@@ -34,12 +34,18 @@ import { Message } from "../common/messages";
/// <reference path="./fallback.d.ts"/>
let F = require("./fallback");
import "chrome";
import { ViewRegistry, loadDescriptors } from "../common/view-registry";
/**
* The data about the tabs
*/
let allTabs: { [index: number]: TabData } = {};
/**
* The reference view registry
*/
let registry: ViewRegistry = new ViewRegistry();
/**
* Gets the data about a tab
* @param id The identifier of a tab
......@@ -381,6 +387,10 @@ chrome.runtime.onMessage.addListener(
data.updateWith(request.payload.tabData);
updateTab(request.payload.tabId);
sendResponse(data);
} else if (request.requestType == "GetViewRegistry") {
sendResponse(registry);
}
}
);
loadDescriptors(registry);
......@@ -71,36 +71,3 @@ export function getValueOf(
undefined
);
}
/**
* Determines whether the RDF store with the specified primary topic matches a trait:
* Has a primary topic
* @param store The RDF store
* @param primaryTopic The primary topic
*/
export function hasTraitHasPrimary(
store: RdfStore,
primaryTopic: string
): boolean {
return primaryTopic != null;
}
/**
* Determines whether the RDF store with the specified primary topic matches a trait:
* The primary topic is a person in DBPedia
* @param store The RDF store
* @param primaryTopic The primary topic
*/
export function hasTraitDbPediaPerson(
store: RdfStore,
primaryTopic: string
): boolean {
if (!hasTraitHasPrimary(store, primaryTopic)) return false;
let a = store.each(
$rdf.sym(primaryTopic),
$rdf.sym("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
$rdf.sym("http://dbpedia.org/ontology/Person"),
undefined
);
return a != null && a != undefined && a.length > 0;
}
......@@ -18,30 +18,24 @@
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
/// <reference path="./rdflib-interface.d.ts"/>
import { RdfStore, Triple, Term, NamedNode, BlankNode, Literal } from "rdflib";
import * as React from "react";
import {
ViewImplementation,
ViewDescriptor,
ViewRenderer,
REGISTRY
} from "../../common/registry";
/// <reference path="../../common/rdflib-interface.d.ts"/>
import { RdfStore, Triple, Term, NamedNode, BlankNode, Literal } from "rdflib";
import * as React from "react";
ViewRegistrySourceInline,
ViewRegistrySourceKind,
ViewLocationKind
} from "./view-registry";
import { $rdf, getValueOf } from "./rdf";
export class NTripleRendering implements ViewImplementation {
export class RdfTriplesRendering implements ViewImplementation {
constructor() {
this.descriptor = {
identifier: "::Defaults::NTripleRendering",
name: "RDF Triples",
description: "Renders a complete RDF dataset using the N-Triples syntax",
origin: null
};
this.priorityFor = this.priorityFor.bind(this);
this.render = this.render.bind(this);
}
descriptor: ViewDescriptor;
priorityFor(store: RdfStore, target: string): number {
return Number.MAX_SAFE_INTEGER;
}
......@@ -70,7 +64,7 @@ export class NTripleRendering implements ViewImplementation {
}}
>
<div className="alert alert-info" role="alert">
{this.descriptor.name}
RDF Triples
</div>
</div>
<table className="table-striped table-condensed">
......@@ -118,6 +112,119 @@ function renderRdfNode(node: Term): React.ReactNode {
return null;
}
export function register(): void {
REGISTRY.register(new NTripleRendering());
/**
* Determines whether the RDF store with the specified primary topic matches a trait:
* Has a primary topic
* @param store The RDF store
* @param primaryTopic The primary topic
*/
export function hasTraitHasPrimary(
store: RdfStore,
primaryTopic: string
): boolean {
return primaryTopic != null;
}
/**
* Determines whether the RDF store with the specified primary topic matches a trait:
* The primary topic is a person in DBPedia
* @param store The RDF store
* @param primaryTopic The primary topic
*/
export function hasTraitDbPediaPerson(
store: RdfStore,
primaryTopic: string
): boolean {
if (!hasTraitHasPrimary(store, primaryTopic)) return false;
let a = store.each(
$rdf.sym(primaryTopic),
$rdf.sym("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
$rdf.sym("http://dbpedia.org/ontology/Person"),
undefined
);
return a != null && a != undefined && a.length > 0;
}
export class DBPediaPersonRendering implements ViewImplementation {
constructor() {
this.priorityFor = this.priorityFor.bind(this);
this.render = this.render.bind(this);
}
priorityFor(store: RdfStore, target: string): number {
return hasTraitDbPediaPerson(store, target) ? 10 : -1;
}
render(
renderer: ViewRenderer,
store: RdfStore,
root: string,
target: string
): React.ReactNode {
let name = getValueOf(store, root, "http://xmlns.com/foaf/0.1/name").value;
return (
<div
style={{
width: "90%",
marginLeft: "5%",
marginTop: "5vh",
marginBottom: "5vh"
}}
>
<div
style={{
width: "75%",
marginLeft: "12.5%",
marginTop: "2vh",
marginBottom: "2vh"
}}
>
<div className="alert alert-info" role="alert">
"DBPedia Person"
</div>
</div>
<div>
<span>Name: {name}</span>
</div>
</div>
);
}
}
/**
* 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
*/
var VIEW_DEFAULTS_TRIPLES: RdfTriplesRendering = new RdfTriplesRendering();
/**
* The implementation for the DbPedia person view
*/
var VIEW_DEFAULTS_DBPEDIA_PERSON: DBPediaPersonRendering = new DBPediaPersonRendering();
......@@ -23,6 +23,39 @@ import { RdfStore } from "rdflib";
import { ReactNode } from "react";
import { Application } from "./application";
/**
* The kind of locations for the location of a view definition
*/
export enum ViewLocationKind {
Embedded = "embedded",
Remote = "remote"
}
/**
* The location of a view definition
*/
export interface ViewLocation {
/**
* The kind of location
*/
kind: ViewLocationKind;
}
/**
* The location of a view definition when already embedded
*/
export interface ViewLocationEmbedded extends ViewLocation {}
/**
* the location of a view definition as a remote script
*/
export interface ViewLocationRemote extends ViewLocation {
/**
* The location of the definition as an URI
*/
uri: string;
}
/**
* The metadata of a view
*/
......@@ -40,19 +73,112 @@ export interface ViewDescriptor {
*/
description: string;
/**
* The views's origin, most likely a URL used to fetch its definition
* The name of the entry point in the view definition
*/
origin: string;
entrypoint: string;
/**
* The location of the view definition
*/
location: ViewLocation;
}
/**
* The implementation of a view
* Kinds of sources for view descriptors for a registry
*/
export interface ViewImplementation {
export enum ViewRegistrySourceKind {
Inlined = "inlined",
Remote = "remote"
}
/**
* A source of view descriptors for a registry
*/
export interface ViewRegistrySource {
/**
* The kind of source
*/
kind: ViewRegistrySourceKind;
/**
* The name of the source
*/
name: string;
}
/**
* A source of view descriptors with the descriptors inlined within it
*/
export interface ViewRegistrySourceInline extends ViewRegistrySource {
/**
* The descriptor (meta-data)
* The inlined descriptors
*/
descriptor: ViewDescriptor;
descriptors: ViewDescriptor[];
}
/**
* A remote source of view descriptors
*/
export interface ViewRegistrySourceRemote extends ViewRegistrySource {
/**
* The uri to fetch the descriptors at
*/
uri: string;
}
/**
* Fetches the descriptors for a source
* @param source The source to fetch from
*/
export function fetchDescriptors(
source: ViewRegistrySource
): Promise<ViewDescriptor[]> {
if (source.kind == ViewRegistrySourceKind.Inlined) {
return new Promise<ViewDescriptor[]>(
(
resolve: (result: ViewDescriptor[] | Promise<ViewDescriptor[]>) => void,
reject: (reason: string) => void
) => {
resolve((source as ViewRegistrySourceInline).descriptors);
}
);
} else if (source.kind == ViewRegistrySourceKind.Remote) {
return new Promise<ViewDescriptor[]>(
(
resolve: (result: ViewDescriptor[] | Promise<ViewDescriptor[]>) => void,
reject: (reason: string) => void
) => {
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");
}
resolve(JSON.parse(xmlHttp.responseText));
}
};
xmlHttp.open("GET", (source as ViewRegistrySourceRemote).uri, true);
xmlHttp.setRequestHeader("Accept", "application/json");
xmlHttp.send();
}
);
} else {
return new Promise<ViewDescriptor[]>(
(
resolve: (result: ViewDescriptor[] | Promise<ViewDescriptor[]>) => void,
reject: (reason: string) => void
) => {
reject("Invalid source");
}
);
}
}
/**
* 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
......@@ -74,11 +200,83 @@ export interface ViewImplementation {
): 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;
}
......@@ -90,55 +288,101 @@ export class ViewRegistry {
* Creates an empty registry
*/
constructor() {
this.sources = [];
this.descriptors = {};
this.implementations = {};
this.resolveViewFor = this.resolveViewFor.bind(this);
}
/**
* The sources for this registry
*/
sources: ViewRegistrySource[];
/**
* The registry of descriptors
*/
descriptors: { [id: string]: ViewDescriptor };
/**
* The registry of implementations
* The implementations
*/
implementations: { [id: string]: ViewImplementation };
}
/**
* Registers a view implementation
* @param implementation The implementation to register
*/
register(implementation: ViewImplementation): void {
this.descriptors[implementation.descriptor.identifier] =
implementation.descriptor;
this.implementations[implementation.descriptor.identifier] = implementation;
}
/**
* Loads all descriptors from the registered sources
* @param registry The registry to load
*/
export function loadDescriptors(registry: ViewRegistry) {
registry.descriptors = {};
registry.sources.map((source: ViewRegistrySource) => {
fetchDescriptors(source)
.catch((reason: string) => {
console.log(
"Failed to fetch descriptor from source " +
source.name +
": " +
reason
);
})
.then((descriptors: ViewDescriptor[]) => {
descriptors.map((descriptor: ViewDescriptor) => {
registry.descriptors[descriptor.identifier] = descriptor;
});
});
});
}
/**
* Resoles a view for an RDF datastore and a resource in it
* @param store The RDF store holding the data to be rendered
* @param target The target RDF resource to render
*/
resolveViewFor(store: RdfStore, target: string): ViewImplementation {
let implementations = this.implementations;
let sorted: PrioritizedViewImplementation[] = Object.keys(
this.implementations
)
.map((id: string) => {
let impl = implementations[id];
return {
implementation: implementations[id],
priority: impl.priorityFor(store, target)
};
/**
* 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
);
})
.filter((a: PrioritizedViewImplementation) => a.priority >= 0)
.sort(