[FIX JENKINS-35860] - extension point component type filter (#308)

* Add component type filter for extensionPoints
* Bump js-extensions beta version
* EXTENSIONS -> README
* more documentation for dataType vs. componentType
* modify usage of 'type' to 'dataType' for consistency
* Split ClassMetadataStore, modify extension filtering to a common method
* Separate componentType to its own file
This commit is contained in:
Keith Zantow 2016-07-20 08:57:28 -07:00 committed by GitHub
parent 84692913ad
commit 7eaf71b391
15 changed files with 362 additions and 177 deletions

View File

@ -36,7 +36,7 @@
},
"dependencies": {
"@jenkins-cd/design-language": "0.0.63",
"@jenkins-cd/js-extensions": "0.0.17-beta-1",
"@jenkins-cd/js-extensions": "0.0.19",
"@jenkins-cd/js-modules": "0.0.5",
"@jenkins-cd/sse-gateway": "0.0.6",
"immutable": "3.8.1",

View File

@ -2,7 +2,7 @@ import React, { Component, PropTypes } from 'react';
import { EmptyStateView } from '@jenkins-cd/design-language';
import { actions as selectorActions, testResults as testResultsSelector,
connect, createSelector } from '../redux';
import Extensions from '@jenkins-cd/js-extensions';
import Extensions, { dataType } from '@jenkins-cd/js-extensions';
const EmptyState = () => (
<EmptyStateView tightSpacing>
@ -58,7 +58,7 @@ export class RunDetailsTests extends Component {
<div className="test-result-duration">Duration {testResults.duration}</div>
</div>
<Extensions.Renderer extensionPoint="jenkins.test.result" dataType={testResults} testResults={testResults} />
<Extensions.Renderer extensionPoint="jenkins.test.result" filter={dataType(testResults)} testResults={testResults} />
</div>);
}
}

View File

@ -12,4 +12,4 @@ extensions:
extensionPoint: jenkins.pipeline.run.result
- component: components/testing/TestResults
extensionPoint: jenkins.test.result
type: hudson.tasks.test.TestResult
dataType: hudson.tasks.test.TestResult

View File

@ -26,7 +26,7 @@
},
"dependencies": {
"@jenkins-cd/design-language": "0.0.63",
"@jenkins-cd/js-extensions": "0.0.17-beta-1",
"@jenkins-cd/js-extensions": "0.0.19",
"@jenkins-cd/js-modules": "0.0.5",
"history": "2.0.2",
"immutable": "3.8.1",

View File

@ -57,9 +57,9 @@ exports.initialize = function (oncomplete) {
// Get the extension list metadata from Jenkins.
// Might want to do some flux fancy-pants stuff for this.
const appRoot = document.getElementsByTagName("head")[0].getAttribute("data-appurl");
Extensions.store.init({
Extensions.init({
extensionDataProvider: cb => getURL(`${appRoot}/js-extensions`, rsp => cb(rsp.data)),
typeInfoProvider: (type, cb) => getURL(`${appRoot}/rest/classes/${type}`, cb)
classMetadataProvider: (type, cb) => getURL(`${appRoot}/rest/classes/${type}`, cb)
});
oncomplete();
};

View File

@ -1,62 +0,0 @@
# Jenkins JavaScript Extensions
Jenkins JavasScript Extensions (JSe) in BlueOcean are handled in the UI by the `@jenkins-cd/js-extensions` module.
JSe is based on the extensibility model already established by Jenkins, based on data and views, with the ability to inherit views based on parent data types.
JSe `@jenkins-cd/js-extensions` module exports 2 things:
- `store` - the `ExtensionStore` instance, which must be initialized
- `Renderer` - a React component to conveniently render extensions
### Store API
The `ExtensionStore` API is very simple, all public methods are asynchronous:
- `getExtensions(extensionPointName, [type,] onload)`
This method will async load data and type information as needed, and call the onload handler with a list of extension exports, e.g. the React classes or otherwise exported references.
- `getTypeInfo(type, onload)`
This will return a list of type information, from the [classes API](../blueocean-rest/README.md#classes_API), this method also handles caching results locally.
- `init()`
Required to be called with `{ extensionDataProvider: ..., typeInfoProvider: }` see: [ExtensionStore.js](src/ExtensionStore.js#init) for details. This is currently done in [init.jsx](../blueocean-web/src/main/js/init.jsx), with methods to fetch extension data from `<jenkins-url>/blue/js-extensions/` and type information from `<jenkins-url>/blue/rest/classes/<class-name>`.
### Rendering extension points
The most common usage pattern is to use the exported `Renderer`, specifying the extension point name, any necessary contextual data, and optionally specifying a data type.
import Extensions from '@jenkins-cd/js-extensions';
...
<Extensions.Renderer extensionPoint="jenkins.navigation.top.menu" />
For example, rendering the test results for a build may be scoped to the specific type of test results in this manner:
<Extensions.Renderer extensionPoint="test-results-view" dataType={data._class} testResults={data} />
The `ExtensionRenderer` component optionally uses the [classes API](../blueocean-rest/README.md#classes_API) to look up an appropriate, specific set of views for the data being displayed. This should works seamlessly with other [capabilities](../blueocean-rest/README.md#capabilities).
### Defining extension points
Extensions are defined in a `jenkins-js-extensions.yaml` file in the javascript source directory of a plugin by defining a list of extensions similar to this:
# Extensions in this plugin
extensions:
- component: AboutNavLink
extensionPoint: jenkins.topNavigation.menu
- component: components/tests/AbstractTestResult
extensionPoint: jenkins.test.result
type: hudson.tasks.test.AbstractTestResultAction
Properties are:
- `component`: a module from which the default export will be used
- `extensionPoint`: the extension point name
- `type`: an optional data type this extension handles
For example, the `AboutNavLink` might be defined as a default export:
export default class NavLink extends React.Component {
...
}
Although extensions are not limited to React components, this is the typical usage so far.

View File

@ -1,4 +1,80 @@
# ExtensionPoint npm module
# Jenkins JavaScript Extensions
This module is published via npm so that other plugins, external to the blueocean project, can make use of it.
Plugins can themselves make use of extension points.
Jenkins JavaScript Extensions are extensions which contribute to the UI in Jenkins BlueOcean.
This module is used to define extension points - locations where the application accepts plugin-provided implementations.
This module is also used to process the plugin extension point implementations to provide to BlueOcean.
This module is published via npm as `@jenkins-cd/js-extensions` so that other plugins, external to the blueocean project, can make use of it.
Plugins can themselves make use of extension points.
Jenkins JavaScript Extensions are based on the extensibility model already established by Jenkins, based on data and views, with the ability to inherit views based on parent data types.
Jenkins JavaScript Extensions: `@jenkins-cd/js-extensions` module exports:
- `Renderer` - a React component to conveniently render extensions
- `store` - the `ExtensionStore` instance (which must be initialized before it can be used)
- `classMetadataStore` - class/capability metadata store
- `dataType()` - function for filtering extensions based on the data type
- `componentType()` - function for filtering extensions based on the required component type (e.g. React class)
### ExtensionStore API
The `ExtensionStore` API is very simple, all public methods are asynchronous:
- `getExtensions(extensionPointName, [filter,] onload)`
This method will async load data, filter the extensions based on the provided `filter`s, and call the onload handler with a list of extension exports, e.g. the React classes or otherwise exported references.
`filter` - a filter function currently the module exports the following functions - see the exported functions for the commonly used filters
### ClassMetadataStore API
- `getClassMetadata(dataType, onload)`
This will return a list of type information, from the [classes API](../blueocean-rest/README.md#classes_API), this method also handles caching results locally.
### Rendering extension points
The most common usage pattern is to use the exported `Renderer`, specifying the extension point name, any necessary contextual data, and optionally specifying a data type.
import Extensions from '@jenkins-cd/js-extensions';
...
<Extensions.Renderer extensionPoint="jenkins.navigation.top.menu" />
For example, rendering the test results for a build may be scoped to the specific type of test results in this manner:
<Extensions.Renderer extensionPoint="test-results-view" filter={dataType(data)} testResults={data} />
The `ExtensionRenderer` component optionally uses the [classes API](../blueocean-rest/README.md#classes_API) to look up an appropriate, specific set of views for the data being displayed.
This should works seamlessly with other [capabilities](../blueocean-rest/README.md#capabilities).
### Defining extension point implementations
Extensions are defined in a `jenkins-js-extensions.yaml` file in the javascript source directory of a plugin by defining a list of extensions similar to this:
# Extensions in this plugin
extensions:
- component: AboutNavLink
extensionPoint: jenkins.topNavigation.menu
- component: components/tests/AbstractTestResult
extensionPoint: jenkins.test.result
dataType: hudson.tasks.test.AbstractTestResultAction
Properties are:
- `component`: a module from which the default export will be used
- `extensionPoint`: the extension point name
- `dataType`: an optional Java data type this extension handles
For example, the `AboutNavLink` might be defined as a default export:
export default class NavLink extends React.Component {
...
}
#### Enforcing specific component types
In order to ensure a specific component is returned, an extension point may also use the `componentType` filter - it accepts an object prototype (e.g. an ES6 class), e.g.:
import TestResults from './base-components/TestResults';
...
<Extensions.Renderer extensionPoint="test-view" filter={componentType(TestResults)} ... />
Extensions are not limited to React components.
The `componentType` filter will attempt to match returned components by a series of prototype and typeof checks to appropriately filter returned types including ES6 classes.

View File

@ -1,3 +1,22 @@
// Provide an ExtensionStore & ExtensionRenderer react component
exports.store = require('./dist/ExtensionStore.js').instance;
exports.Renderer = require('./dist/ExtensionRenderer.js').ExtensionRenderer;
exports.classMetadataStore = require('./dist/ClassMetadataStore.js').instance;
exports.dataType = function dataType(dataType) { return exports.classMetadataStore.dataType(dataType); };
exports.untyped = function untyped() { return exports.classMetadataStore.untyped(); };
exports.isType = require('./dist/ComponentTypeFilter.js').isType;
exports.componentType = require('./dist/ComponentTypeFilter.js').componentType;
exports.init = function init(args) {
exports.classMetadataStore.init(args.classMetadataProvider);
exports.store.init({
extensionDataProvider: args.extensionDataProvider,
classMetadataStore: exports.classMetadataStore,
});
};

View File

@ -1,6 +1,6 @@
{
"name": "@jenkins-cd/js-extensions",
"version": "0.0.17-beta-1",
"version": "0.0.19",
"description": "Jenkins Extension Store",
"main": "index.js",
"files": [

View File

@ -1,19 +1,26 @@
var jsTest = require('@jenkins-cd/js-test');
var expect = require('chai').expect;
var ExtensionStore = require('../dist/ExtensionStore').ExtensionStore;
var ClassMetadataStore = require('../dist/ClassMetadataStore').instance;
var componentType = require('../dist/ComponentTypeFilter').componentType;
var javaScriptExtensionInfo = require('./javaScriptExtensionInfo-01.json');
// js modules calling console.debug
console.debug = function(msg) { console.log('DEBUG: ' + msg); };
var mockDataLoad = function(extensionStore, out) {
var jsModules = require('@jenkins-cd/js-modules');
// Mock the calls to import
var jsModules = require('@jenkins-cd/js-modules');
var theRealImport = jsModules.import;
var makeClassMetadataStore = function(fn) {
ClassMetadataStore.init(fn);
return ClassMetadataStore;
};
var mockDataLoad = function(extensionStore, out, componentMap) {
out.plugins = {};
out.loadCount = 0;
// Mock the calls to import
var theRealImport = jsModules.import;
jsModules.import = function(bundleId) {
var internal = require('@jenkins-cd/js-modules/js/internal');
var bundleModuleSpec = internal.parseResourceQName(bundleId);
@ -25,7 +32,11 @@ var mockDataLoad = function(extensionStore, out) {
if (pluginMetadata.hpiPluginId === pluginId) {
var extensions = pluginMetadata.extensions;
for(var i2 = 0; i2 < extensions.length; i2++) {
extensionStore._registerComponentInstance(extensions[i2].extensionPoint, pluginMetadata.hpiPluginId, extensions[i2].component, extensions[i2].component);
var component = extensions[i2].component;
if (componentMap && component in componentMap) {
component = componentMap[component];
}
extensionStore._registerComponentInstance(extensions[i2].extensionPoint, pluginMetadata.hpiPluginId, extensions[i2].component, component);
}
}
}
@ -68,7 +79,7 @@ describe("ExtensionStore.js", function () {
extensionStore.init({
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
typeInfoProvider: function(type, cb) { cb({}); }
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb({}); })
});
extensionStore.getExtensions('ep-1', function(extensions) {
@ -89,7 +100,7 @@ describe("ExtensionStore.js", function () {
// this info will be loaded from <jenkins>/blue/js-extensions/
extensionStore.init({
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
typeInfoProvider: function(type, cb) { cb({}); }
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb({}); })
});
// Call load for ExtensionPoint impls 'ep-1'. This should mimic
@ -146,21 +157,21 @@ describe("ExtensionStore.js", function () {
extensionStore.init({
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
typeInfoProvider: function(type, cb) { cb(typeData[type]); }
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb(typeData[type]); })
});
extensionStore.getExtensions('ept-1', 'type-1', function(extensions) {
expect(extensionStore.typeInfo).to.not.be.undefined;
extensionStore.getExtensions('ept-1', [ClassMetadataStore.dataType('type-1')], function(extensions) {
expect(extensions.length).to.equal(1);
expect(extensions[0]).to.equal('typed-component-1.1');
});
extensionStore.getExtensions('ept-2', 'type-2', function(extensions) {
extensionStore.getExtensions('ept-2', [ClassMetadataStore.dataType('type-2')], function(extensions) {
expect(extensions.length).to.equal(1);
expect(extensions).to.include.members(["typed-component-1.2"]);
done();
});
done();
});
it("- handles untyped extension points", function(done) {
@ -191,20 +202,20 @@ describe("ExtensionStore.js", function () {
extensionStore.init({
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
typeInfoProvider: function(type, cb) { cb(typeData[type]); }
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb(typeData[type]); })
});
extensionStore.getExtensions('ep-1', function(extensions) {
extensionStore.getExtensions('ep-1', [ClassMetadataStore.untyped()], function(extensions) {
expect(extensions.length).to.equal(3);
expect(extensions).to.include.members(["component-1.1","component-1.2","component-2.1"]);
});
extensionStore.getExtensions('ept-2', function(extensions) {
extensionStore.getExtensions('ept-2', [ClassMetadataStore.untyped()], function(extensions) {
expect(extensions.length).to.equal(0);
expect(extensions).to.include.members([]);
done();
});
done();
});
it("- handles multi-key requests", function(done) {
@ -215,7 +226,7 @@ describe("ExtensionStore.js", function () {
extensionStore.init({
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
typeInfoProvider: function(type, cb) { cb({}); }
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb({}); })
});
extensionStore.getExtensions(['ep-1','ep-2'], function(ep1,ep2) {
@ -228,4 +239,50 @@ describe("ExtensionStore.js", function () {
done();
});
it("- handles componentType", function(done) {
var extensionStore = new ExtensionStore();
class PretendReactClass {
}
class PretendComponent1 extends PretendReactClass {
}
class PretendComponent2 extends PretendReactClass {
}
var plugins = {};
mockDataLoad(extensionStore, plugins, {
'component-1.3.1': PretendComponent1,
'component-2.3.1': PretendComponent2,
});
extensionStore.init({
extensionDataProvider: function(cb) { cb(javaScriptExtensionInfo); },
classMetadataStore: makeClassMetadataStore(function(type, cb) { cb({}); })
});
extensionStore.getExtensions('ep-3', [componentType(PretendComponent1)], function(extensions) {
expect(extensions.length).to.equal(1);
expect(extensions).to.include.members([PretendComponent1]);
});
extensionStore.getExtensions('ep-3', [componentType(PretendComponent2)], function(extensions) {
expect(extensions.length).to.equal(1);
expect(extensions).to.include.members([PretendComponent2]);
});
extensionStore.getExtensions('ep-3', [componentType(PretendReactClass)], function(extensions) {
expect(extensions.length).to.equal(2);
expect(extensions).to.include.members([PretendComponent1, PretendComponent2]);
});
extensionStore.getExtensions('ep-3', [componentType(PretendReactClass), componentType(PretendComponent1)], function(extensions) {
expect(extensions.length).to.equal(1);
expect(extensions).to.include.members([PretendComponent1]);
});
done();
});
});

View File

@ -12,6 +12,10 @@
{
"component": "component-1.3",
"extensionPoint": "ep-2"
},
{
"component": "component-1.3.1",
"extensionPoint": "ep-3"
}
],
"hpiPluginId": "plugin-1"
@ -29,6 +33,10 @@
{
"component": "component-2.3",
"extensionPoint": "ep-2"
},
{
"component": "component-2.3.1",
"extensionPoint": "ep-3"
}
],
"hpiPluginId": "plugin-2"
@ -38,22 +46,22 @@
{
"component": "typed-component-1.1",
"extensionPoint": "ept-1",
"type": "supertype-1"
"dataType": "supertype-1"
},
{
"component": "typed-component-1.2",
"extensionPoint": "ept-2",
"type": "type-2"
"dataType": "type-2"
},
{
"component": "typed-component-1.2.1",
"extensionPoint": "ept-2",
"type": "subtype-2"
"dataType": "subtype-2"
},
{
"component": "typed-component-1.2.2",
"extensionPoint": "ept-2",
"type": "supertype-2"
"dataType": "supertype-2"
}
],
"hpiPluginId": "plugin-3",

View File

@ -0,0 +1,81 @@
/**
* ClassMetadataStore is responsible for maintaining extension metadata
* including type/capability info
*/
export class ClassMetadataStore {
init(classMetadataProvider) {
/**
* Type info cache
*/
this.classMetadata = {};
/**
* Fetch function for the classMetadata
*/
this.classMetadataProvider = classMetadataProvider;
}
/**
* Gets the type/capability info for the given data type
*/
getClassMetadata(type, onload) {
var classMeta = this.classMetadata[type];
if (classMeta) {
return onload(classMeta);
}
this.classMetadataProvider(type, (data) => {
classMeta = this.classMetadata[type] = JSON.parse(JSON.stringify(data));
classMeta.classes = classMeta.classes || [];
// Make sure the type itself is in the list
if (classMeta.classes.indexOf(type) < 0) {
classMeta.classes = [type, ...classMeta.classes];
}
onload(classMeta);
});
}
dataType(dataType) {
return (extensions, onload) => {
if (dataType && typeof(dataType) === 'object'
&& '_class' in dataType) { // handle the common API incoming data
dataType = dataType._class;
}
this.getClassMetadata(dataType, (currentTypeInfo) => {
// prevent returning extensions for the given type
// when a more specific extension is found
var matchingExtensions = [];
eachType: for (var typeIndex = 0; typeIndex < currentTypeInfo.classes.length; typeIndex++) {
// currentTypeInfo.classes is ordered by java hierarchy, including
// and beginning with the current data type
var type = currentTypeInfo.classes[typeIndex];
for (var i = 0; i < extensions.length; i++) {
var extension = extensions[i];
if (type === extension.dataType) {
matchingExtensions.push(extension);
}
}
// if we have this specific type handled, don't
// proceed to parent types
if (matchingExtensions.length > 0) {
break eachType;
}
}
onload(matchingExtensions);
});
};
}
/**
* Returns a filtering function to only return untyped extensions
*/
untyped() {
return (extensions, onload) => {
// exclude typed extensions when types not requested
extensions = extensions.filter(m => !('dataType' in m));
onload(extensions);
};
}
}
export const instance = new ClassMetadataStore();

View File

@ -0,0 +1,39 @@
/**
* Tries to determine if the objectToTest is of the given type.
* Will normalize things like String/'string' inconsistencies
* as well as ES6 class & traditional prototype inheritance.
* NOTE: This ALSO tests the prototype hierarchy if objectToTest
* is a Function.
*/
export function isType(objectToTest, type) {
var o = objectToTest;
if (typeof o === type) {
return true;
}
if (type === String || type === 'string') {
return o instanceof String;
}
if (type === Function || type === 'function') {
return o instanceof Function;
}
if (type === Object || type === 'object') {
return o instanceof Object;
}
if (objectToTest instanceof Function) {
var proto = objectToTest;
while (proto) {
if (proto === type) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
}
return objectToTest instanceof type;
}
export function componentType(componentType) {
return (extensions, onload) => {
extensions = extensions.filter(e => isType(e.instance, componentType));
onload(extensions);
};
}

View File

@ -1,6 +1,6 @@
var React = require('react');
var ReactDOM = require('react-dom');
var ExtensionStore = require('./ExtensionStore.js');
var ExtensionStore = require('./ExtensionStore.js').instance;
var ResourceLoadTracker = require('./ResourceLoadTracker').instance;
/**
@ -62,7 +62,7 @@ export class ExtensionRenderer extends React.Component {
}
_setExtensions() {
ExtensionStore.instance.getExtensions(this.props.extensionPoint, this.props.dataType,
ExtensionStore.getExtensions(this.props.extensionPoint, this.props.filter,
extensions => this.setState({extensions: extensions})
);
}
@ -164,7 +164,7 @@ ExtensionRenderer.defaultProps = {
ExtensionRenderer.propTypes = {
extensionPoint: React.PropTypes.string.isRequired,
dataType: React.PropTypes.any,
filter: React.PropTypes.any,
wrappingElement: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element])
};

View File

@ -7,7 +7,9 @@ export class ExtensionStore {
* FIXME this is NOT a constructor, as there's no common way to
* pass around a DI singleton at the moment across everything
* that needs it (e.g. redux works for the app, not for other
* things in this module)
* things in this module).
*
* NOTE: this is currently called from `blueocean-web/src/main/js/init.jsx`
*
* Needs:
* args = {
@ -15,9 +17,11 @@ export class ExtensionStore {
* ... // get the data
* callback(extensionData); // array of extensions
* },
* typeInfoProvider: (type, callback) => {
* ... // get the data based on 'type'
* callback(typeInfo);
* classMetadataStore: {
* getClassMetadata(dataType, callback) => {
* ... // get the data based on 'dataType'
* callback(typeInfo);
* }
* }
* }
*/
@ -29,14 +33,10 @@ export class ExtensionStore {
* The registered ExtensionPoint metadata + instance refs
*/
this.extensionPoints = {};
/**
* Type info cache
*/
this.typeInfo = {};
/**
* Used to fetch type information
*/
this.typeInfoProvider = args.typeInfoProvider;
this.classMetadataStore = args.classMetadataStore;
}
/**
@ -53,7 +53,7 @@ export class ExtensionStore {
extension.instance = instance;
return;
}
throw `Unable to locate plugin for ${extensionPointId} / ${pluginId} / ${component}`;
throw new Error(`Unable to locate plugin for ${extensionPointId} / ${pluginId} / ${component}`);
}
/**
@ -76,102 +76,69 @@ export class ExtensionStore {
* will call the onload callback with a list of exported extension
* objects (e.g. React classes or otherwise).
*/
getExtensions(key, type, onload) {
if (!this.extensionDataProvider) {
throw "Must call ExtensionStore.init({ extensionDataProvider: (cb) => ..., typeInfoProvider: (type, cb) => ... }) first";
}
getExtensions(extensionPoint, filter, onload) {
// Allow calls like: getExtensions('something', a => ...)
if (arguments.length === 2 && typeof(type) === 'function') {
onload = type;
type = undefined;
if (arguments.length === 2 && typeof(filter) === 'function') {
onload = filter;
filter = undefined;
}
// And calls like: getExtensions(['a','b'], (a,b) => ...)
if (key instanceof Array) {
var keys = key;
if (extensionPoint instanceof Array) {
var args = [];
var nextArg = ext => {
if(ext) args.push(ext);
if (keys.length === 0) {
if (extensionPoint.length === 0) {
onload(...args);
} else {
var arg = keys[0];
keys = keys.slice(1);
this.getExtensions(arg, null, nextArg);
var arg = extensionPoint[0];
extensionPoint = extensionPoint.slice(1);
this.getExtensions(arg, filter, nextArg);
}
};
nextArg();
return;
}
this._loadBundles(key, extensions => this._filterExtensions(extensions, key, type, onload));
this._loadBundles(extensionPoint, extensions => this._filterExtensions(extensions, filter, onload));
}
/**
* Gets the type/capability info for the given data type
*/
getTypeInfo(type, onload) {
var ti = this.typeInfo[type];
if (ti) {
return onload(ti);
}
this.typeInfoProvider(type, (data) => {
ti = this.typeInfo[type] = JSON.parse(JSON.stringify(data));
ti.classes = ti.classes || [];
if (ti.classes.indexOf(type) < 0) {
ti.classes = [type, ...ti.classes];
}
onload(ti);
});
}
_filterExtensions(extensions, key, currentDataType, onload) {
if (currentDataType && typeof(currentDataType) === 'object'
&& '_class' in currentDataType) { // handle the common API incoming data
currentDataType = currentDataType._class;
}
_filterExtensions(extensions, filters, onload) {
if (extensions.length === 0) {
onload(extensions); // no extensions for the given key
onload(extensions); // no extensions to filter
return;
}
if (currentDataType) {
var currentTypeInfo = this.typeInfo[currentDataType];
if (!currentTypeInfo) {
this.getTypeInfo(currentDataType, () => {
this._filterExtensions(extensions, key, currentDataType, onload);
});
return;
if (filters) {
// allow calls like: getExtensions('abcd', dataType(something), ext => ...)
if (!filters.length) {
filters = [ filters ];
}
// prevent returning extensions for the given type
// when a more specific extension is found
var matchingExtensions = [];
eachType: for (var typeIndex = 0; typeIndex < currentTypeInfo.classes.length; typeIndex++) {
// currentTypeInfo.classes is ordered by java hierarchy, including
// and beginning with the current data type
var type = currentTypeInfo.classes[typeIndex];
for (var i = 0; i < extensions.length; i++) {
var extension = extensions[i];
if (type === extension.type) {
matchingExtensions.push(extension);
}
var remaining = [].concat(filters);
var nextFilter = extensions => {
if (remaining.length === 0) {
// Map to instances and proceed
onload(extensions.map(m => m.instance));
} else {
var filter = remaining[0];
remaining = remaining.slice(1);
filter(extensions, nextFilter);
}
// if we have this specific type handled, don't
// proceed to parent types
if (matchingExtensions.length > 0) {
break eachType;
}
}
extensions = matchingExtensions;
};
nextFilter(extensions);
} else {
// exclude typed extensions when types not requested
extensions = extensions.filter(m => !('type' in m));
// Map to instances and proceed
onload(extensions.map(m => m.instance));
}
onload(extensions.map(m => m.instance));
}
/**
* Fetch all the extension data
*/
_loadExtensionData(oncomplete) {
if (!this.extensionDataProvider) {
throw new Error("Must call ExtensionStore.init({ extensionDataProvider: (cb) => ..., typeInfoProvider: (type, cb) => ... }) first");
}
if (this.extensionPointList) {
onconplete(this.extensionPointList);
return;