Skip to content

Commit

Permalink
feat: allow defining capabilities with optional qualifier entries
Browse files Browse the repository at this point in the history
The platform supports the following wildcard characters as qualifier
entry value:
- question mark (?): makes the qualifier entry optional 
- asterisk (*): makes the qualifier entry mandatory allowing to provide
any value (except `null` or `undefined`)

closes: #154, #173

BREAKING CHANGE:
- removed support for the asterisk (*) wildcard as capability qualifier
key: instead, use the question mark (?) as qualifier value to mark the
qualifier entry as optional
  • Loading branch information
mofogasy authored and danielwiehl committed Aug 30, 2019
1 parent cd41eb3 commit d462512
Show file tree
Hide file tree
Showing 20 changed files with 577 additions and 220 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,6 @@ export interface Capability {
* Symbolic name of the application which provides this capability.
*/
symbolicAppName: string;
/**
* Indicates if the capability implementor acts as a proxy through which intents are processed.
*
* For example, `ViewIntentHandler` is a proxy for application view capabilities which
* reads config from registered view capability providers and dispatches intents to the Angular router.
*/
proxy: boolean;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ export class ActivityRegistrator {
*/
public init(): void {
this._manifestCollector.whenManifests.then(manifestRegistry => {
const activityCapabilities: ActivityCapability[] = manifestRegistry.getCapabilitiesByType<ActivityCapability>(PlatformCapabilityTypes.Activity)
.filter(capability => !capability.metadata.proxy);
const activityCapabilities: ActivityCapability[] = manifestRegistry.getCapabilitiesByType<ActivityCapability>(PlatformCapabilityTypes.Activity);
this.installActivityCapabilityRoutes(activityCapabilities);
this.registerActivities(activityCapabilities);
});
Expand All @@ -46,7 +45,6 @@ export class ActivityRegistrator {
this._routesRegistrator.replaceRouterConfig([
...this._router.config,
...activityCapabilities
.filter(activityCapability => !activityCapability.metadata.proxy)
.map((activityCapability: ActivityCapability): Route => {
return {
path: `${activityCapability.metadata.symbolicAppName}/${activityCapability.metadata.id}`,
Expand All @@ -62,7 +60,6 @@ export class ActivityRegistrator {

private registerActivities(activityCapabilities: ActivityCapability[]): void {
activityCapabilities
.filter(activityCapability => !activityCapability.metadata.proxy)
.forEach(activityCapability => {
const activity = this._activityPartService.createActivity();
activity.title = activityCapability.properties.title;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
* SPDX-License-Identifier: EPL-2.0
*/

import { QueryList } from '@angular/core';

/**
* Provides array utlity methods.
* Provides array utility methods.
*/
export class Arrays {

Expand All @@ -25,4 +27,54 @@ export class Arrays {
}
return Array.isArray(value) ? value : [value];
}

/**
* Compares items of given arrays for reference equality.
*
* Use the parameter `exactOrder` to control if the item order must be equal.
*/
public static equal(array1: any[], array2: any[], exactOrder: boolean = true): boolean {
if (array1 === array2) {
return true;
}

if (array1.length !== array2.length) {
return false;
}

return array1.every((item, index) => {
if (exactOrder) {
return item === array2[index];
}
else {
return array2.includes(item);
}
});
}

/**
* Finds the last item matching the given predicate, if any,
* or returns the last item in the array if no predicate is specified.
*
* Returns `undefined` if no element is found.
*/
public static last<T>(items: T[] | QueryList<T>, predicate?: (item: T) => boolean): T | undefined {
const array = items ? (Array.isArray(items) ? items : items.toArray()) : [];

if (!predicate) {
return array[array.length - 1];
}
return [...array].reverse().find(predicate);
}

/**
* Removes given item from the array. The original array will not be modified.
*/
public static remove<T>(items: T[], item: T): T[] {
const result = [...items];
for (let index = result.indexOf(item); index !== -1; index = result.indexOf(item)) {
result.splice(index, 1);
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ import { Subject } from 'rxjs';
import { ManifestRegistry } from './manifest-registry.service';
import { filter, takeUntil } from 'rxjs/operators';
import { MessageBus } from './message-bus.service';
import { ApplicationRegistry } from './application-registry.service';
import { MessageEnvelope, NilQualifier } from '@scion/workbench-application-platform.api';
import { testQualifier } from './qualifier-tester';
import { matchesIntentQualifier } from './qualifier-tester';

/**
* Registers intent handlers registered via {INTENT_HANDLER} DI injection token.
Expand All @@ -30,7 +29,6 @@ export class IntentHandlerRegistrator implements OnDestroy {
private _destroy$ = new Subject<void>();

constructor(@Inject(INTENT_HANDLER) private _handlers: IntentHandler[],
private _applicationRegistry: ApplicationRegistry,
private _manifestRegistry: ManifestRegistry,
private _messageBus: MessageBus,
private _logger: Logger) {
Expand All @@ -50,14 +48,14 @@ export class IntentHandlerRegistrator implements OnDestroy {
qualifier: handler.qualifier || NilQualifier,
private: false,
description: handler.description,
}], handler.proxy);
}]);

handler.onInit && handler.onInit(this._applicationRegistry, this._manifestRegistry);
handler.onInit && handler.onInit();

this._messageBus.receiveIntentsForApplication$(HOST_APPLICATION_SYMBOLIC_NAME)
.pipe(
filter(envelope => envelope.message.type === handler.type),
filter(envelope => testQualifier(handler.qualifier, envelope.message.qualifier)),
filter(envelope => matchesIntentQualifier(handler.qualifier, envelope.message.qualifier)),
takeUntil(this._destroy$),
)
.subscribe((envelope: MessageEnvelope) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import { Injectable } from '@angular/core';
import { Capability, Intent, Qualifier } from '@scion/workbench-application-platform.api';
import { Defined } from './defined.util';
import { sha256 } from 'js-sha256';
import { testQualifier } from './qualifier-tester';

const NilSelector = (capability: Capability): boolean => true;
import { matchesCapabilityQualifier, matchesIntentQualifier } from './qualifier-tester';

/**
* Registry with all registered application capabilities and intents.
Expand Down Expand Up @@ -53,7 +51,22 @@ export class ManifestRegistry {
* Returns capabilities which have the given required type and qualifiers.
*/
public getCapabilities<T extends Capability>(type: string, qualifier: Qualifier): T[] {
return this.getCapabilitiesByType(type).filter(capability => testQualifier(capability.qualifier, qualifier)) as T[];
return this.getCapabilitiesByType(type)
.filter(capability => matchesCapabilityQualifier(capability.qualifier, qualifier)) as T[];
}

/**
* Returns capabilities which match the given predicate.
*/
public getCapabilitiesByPredicate(predicate: (capability: Capability) => boolean): Capability[] {
return Array.from(this._capabilitiesById.values()).filter(predicate);
}

/**
* Checks if the given capability is visible to the given application.
*/
public isVisibleForApplication(capability: Capability, symbolicName: string): boolean {
return !capability.private || this.isScopeCheckDisabled(symbolicName) || capability.metadata.symbolicAppName === symbolicName;
}

/**
Expand All @@ -71,53 +84,51 @@ export class ManifestRegistry {
}

/**
* Tests if the application has registered an intent for the given type and qualifiers,
* Tests if the specified application has registered an intent for the given type and qualifiers,
* or whether the application has an implicit intent because it provides the capability itself.
*/
public hasIntent(symbolicName: string, type: string, qualifier: Qualifier): boolean {
return this.getIntentsByApplication(symbolicName).some(intent => {
return intent.type === type && testQualifier(intent.qualifier, qualifier);
return intent.type === type && matchesIntentQualifier(intent.qualifier, qualifier);
});
}

/**
* Tests if the application has registered a capability for the given type and qualifiers,
* and if specified, which also matches the selector function.
* Tests if the specified application has registered a capability for the given type and qualifiers.
*/
public hasCapability(symbolicName: string, type: string, qualifier: Qualifier, selector: (capability: Capability) => boolean = NilSelector): boolean {
const capabilities = this.getCapabilities(type, qualifier);
return capabilities.some(capability => capability.metadata.symbolicAppName === symbolicName && selector(capability));
public hasCapability(symbolicName: string, type: string, qualifier: Qualifier): boolean {
return this.getCapabilities(type, qualifier)
.some(capability => capability.metadata.symbolicAppName === symbolicName);
}

/**
* Tests if some application is capable of handling the given intent.
* The capability must be provided with public visibility unless provided by the requesting application itself.
*/
public isHandled(symbolicName: string, type: string, qualifier: Qualifier): boolean {
return this.getCapabilities(type, qualifier)
.filter(capability => !capability.metadata.proxy)
.some(capability => !capability.private || this.isScopeCheckDisabled(symbolicName) || capability.metadata.symbolicAppName === symbolicName);
return this.getCapabilities(type, qualifier).some(capability => this.isVisibleForApplication(capability, symbolicName));
}

/**
* Registers capabilities of the given application.
*
* The parameter 'proxy' indicates the implementor act as a proxy through which intents are processed (which by default is false).
*/
public registerCapability(symbolicName: string, capabilities: Capability[], proxy: boolean = false): void {
public registerCapability(symbolicName: string, capabilities: Capability[]): void {
if (!capabilities || !capabilities.length) {
return;
}

capabilities.forEach(it => {
if (it.hasOwnProperty('*')) {
throw Error(`[CapabilityRegistrationError] Capability qualifiers do not support \`*\` as key`);
}

const registeredCapabilities = this._capabilitiesByType.get(it.type) || [];
const capability: Capability = {
...it,
private: Defined.orElse(it.private, true),
metadata: {
id: sha256(JSON.stringify({application: symbolicName, type: it.type, ...it.qualifier})).substr(0, 7), // use the first 7 digits of the capability hash as capability id
symbolicAppName: symbolicName,
proxy: proxy,
},
};

Expand All @@ -126,9 +137,7 @@ export class ManifestRegistry {
});

// Register implicit intents. These are intents for capabilities which the application provides itself.
if (!proxy) {
this.registerIntents(symbolicName, capabilities.map(capability => ({type: capability.type, qualifier: capability.qualifier})), true);
}
this.registerIntents(symbolicName, capabilities.map(capability => ({type: capability.type, qualifier: capability.qualifier})), true);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { Defined } from './defined.util';
import { ManifestRegistry } from './manifest-registry.service';
import { ApplicationRegistry } from './application-registry.service';
import { UUID } from './uuid.util';
import { Capability, CapabilityProviderMessage, Channel, IntentMessage, MessageEnvelope, PROTOCOL } from '@scion/workbench-application-platform.api';
import { CapabilityProviderMessage, Channel, IntentMessage, MessageEnvelope, PROTOCOL } from '@scion/workbench-application-platform.api';
import { matchesCapabilityQualifier } from './qualifier-tester';

/**
* Allows communication between the workbench applications.
Expand Down Expand Up @@ -137,16 +138,19 @@ export class MessageBus implements OnDestroy {
filterByChannel('intent'),
filter(envelope => {
const message = envelope.message as IntentMessage;

// To receive the intent, the capability must have public visibility or provided by the intending application itself.
const selector = (capability: Capability): boolean => {
return !capability.private || this._manifestRegistry.isScopeCheckDisabled(symbolicName) || envelope.sender === symbolicName;
};

return this._manifestRegistry.hasCapability(symbolicName, message.type, message.qualifier, selector);
return this._manifestRegistry.getCapabilitiesByApplication(symbolicName)
.filter(capability => matchesCapabilityQualifier(capability.qualifier, message.qualifier))
.some(capability => this._manifestRegistry.isVisibleForApplication(capability, envelope.sender));
}));
}

/**
* Receives all intent messages.
*/
public receiveIntents$(): Observable<MessageEnvelope<IntentMessage>> {
return this._stream$.pipe(filterByChannel('intent'));
}

/**
* Receives messages posted by applications providing a capability which the specified application has registered an intent for,
* or if the receiving application is implicitly eligible because providing the capability itself.
Expand All @@ -163,12 +167,9 @@ export class MessageBus implements OnDestroy {
return false;
}

// To receive a message from a capability provider, the capability must have public visibility or provided by the application itself.
return envelope.sender === symbolicName
|| this._manifestRegistry.isScopeCheckDisabled(symbolicName)
|| this._manifestRegistry.getCapabilities(message.type, message.qualifier)
.filter(capability => capability.metadata.symbolicAppName === envelope.sender)
.some(capability => !capability.private);
return this._manifestRegistry.getCapabilities(message.type, message.qualifier)
.filter(capability => capability.metadata.symbolicAppName === envelope.sender)
.some(capability => this._manifestRegistry.isVisibleForApplication(capability, symbolicName));
}));
}

Expand All @@ -178,7 +179,7 @@ export class MessageBus implements OnDestroy {
public receiveReplyMessagesForApplication$(symbolicName: string): Observable<MessageEnvelope> {
return this._stream$.pipe(
filterByChannel('reply'),
filter(envelope => envelope.replyTo === symbolicName)
filter(envelope => envelope.replyTo === symbolicName),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
*/

import { InjectionToken } from '@angular/core';
import { ApplicationRegistry } from './application-registry.service';
import { ManifestRegistry } from './manifest-registry.service';
import { Capability, Intent, IntentMessage, MessageEnvelope, Qualifier } from '@scion/workbench-application-platform.api';
import { Observable } from 'rxjs';

Expand Down Expand Up @@ -101,7 +99,7 @@ export interface ApplicationManifest {
/**
* Handles intents of a specific type and qualifiers.
*
* There are some built-in handlers installed by the platform: 'view', 'popup', 'messagebox', 'notification' and 'manifest-registry'.
* There are some built-in handlers installed by the platform: 'messagebox', 'notification' and 'manifest-registry'.
*
* To install a handler, register it via DI token {INTENT_HANDLER} as multi provider in the host application.
*
Expand All @@ -126,29 +124,19 @@ export interface IntentHandler {
readonly type: string;

/**
* Optional qualifiers which this handler requires. If not specified, {NilQualifier} is used.
* Qualifier which this handler requires. If not specified, {NilQualifier} is used.
*/
readonly qualifier?: Qualifier;
readonly qualifier: Qualifier;

/**
* Describes the capability this handler handles.
*/
readonly description: string;

/**
* Indicates if this handler acts as a proxy through which intents are processed.
*
* For example, `ViewIntentHandler` is a proxy for application view capabilities which
* reads config from registered view capability providers and dispatches intents to the Angular router.
*/
readonly proxy?: boolean;

/**
* A lifecycle hook that is called after the platform completed registration of applications.
*
* Use this method to handle any initialization tasks which require the application or manifest registry.
*/
onInit?(applicationRegistry: ApplicationRegistry, manifestRegistry: ManifestRegistry): void;
onInit?(): void;

/**
* Method invoked upon the receipt of an intent which this handler qualifies to receive.
Expand Down
Loading

0 comments on commit d462512

Please sign in to comment.