diff --git a/projects/scion/workbench-application-platform.api/src/lib/core.model.ts b/projects/scion/workbench-application-platform.api/src/lib/core.model.ts index 8ebc037bd..82d1d16dd 100644 --- a/projects/scion/workbench-application-platform.api/src/lib/core.model.ts +++ b/projects/scion/workbench-application-platform.api/src/lib/core.model.ts @@ -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; }; } diff --git a/projects/scion/workbench-application-platform/src/lib/activity-capability/activity-registrator.service.ts b/projects/scion/workbench-application-platform/src/lib/activity-capability/activity-registrator.service.ts index b7e716b5b..671d1fdb0 100644 --- a/projects/scion/workbench-application-platform/src/lib/activity-capability/activity-registrator.service.ts +++ b/projects/scion/workbench-application-platform/src/lib/activity-capability/activity-registrator.service.ts @@ -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); }); @@ -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}`, @@ -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; diff --git a/projects/scion/workbench-application-platform/src/lib/core/array.util.ts b/projects/scion/workbench-application-platform/src/lib/core/array.util.ts index 917fd72f8..0d9871420 100644 --- a/projects/scion/workbench-application-platform/src/lib/core/array.util.ts +++ b/projects/scion/workbench-application-platform/src/lib/core/array.util.ts @@ -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 { @@ -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; + } } diff --git a/projects/scion/workbench-application-platform/src/lib/core/intent-handler-registrator.service.ts b/projects/scion/workbench-application-platform/src/lib/core/intent-handler-registrator.service.ts index 928905dd1..0a7e289ad 100644 --- a/projects/scion/workbench-application-platform/src/lib/core/intent-handler-registrator.service.ts +++ b/projects/scion/workbench-application-platform/src/lib/core/intent-handler-registrator.service.ts @@ -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. @@ -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) { @@ -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) => { diff --git a/projects/scion/workbench-application-platform/src/lib/core/manifest-registry.service.ts b/projects/scion/workbench-application-platform/src/lib/core/manifest-registry.service.ts index 08649e5a0..4aff1a8be 100644 --- a/projects/scion/workbench-application-platform/src/lib/core/manifest-registry.service.ts +++ b/projects/scion/workbench-application-platform/src/lib/core/manifest-registry.service.ts @@ -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. @@ -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; } /** @@ -71,22 +84,21 @@ 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); } /** @@ -94,22 +106,22 @@ export class ManifestRegistry { * 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, @@ -117,7 +129,6 @@ export class ManifestRegistry { 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, }, }; @@ -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); } /** diff --git a/projects/scion/workbench-application-platform/src/lib/core/message-bus.service.ts b/projects/scion/workbench-application-platform/src/lib/core/message-bus.service.ts index 15733c4f9..d7850e333 100644 --- a/projects/scion/workbench-application-platform/src/lib/core/message-bus.service.ts +++ b/projects/scion/workbench-application-platform/src/lib/core/message-bus.service.ts @@ -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. @@ -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. @@ -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)); })); } @@ -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), ); } diff --git a/projects/scion/workbench-application-platform/src/lib/core/metadata.ts b/projects/scion/workbench-application-platform/src/lib/core/metadata.ts index 78c773c11..2a24ab379 100644 --- a/projects/scion/workbench-application-platform/src/lib/core/metadata.ts +++ b/projects/scion/workbench-application-platform/src/lib/core/metadata.ts @@ -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'; @@ -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. * @@ -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. diff --git a/projects/scion/workbench-application-platform/src/lib/core/qualifier-tester.ts b/projects/scion/workbench-application-platform/src/lib/core/qualifier-tester.ts index f05684695..7c1c85e01 100644 --- a/projects/scion/workbench-application-platform/src/lib/core/qualifier-tester.ts +++ b/projects/scion/workbench-application-platform/src/lib/core/qualifier-tester.ts @@ -11,27 +11,75 @@ import { NilQualifier, Qualifier } from '@scion/workbench-application-platform.api'; /** - * Tests if the qualifier matches the qualifier pattern. + * Tests if the given qualifier matches the capability qualifier. * - * @param pattern - * qualifier as specified in the manifest, may contain wildcards as qualifier key or/and qualifier value; - * if `null`, {AnyQualifier} is used. + * @param capabilityQualifier + * qualifier for a capability as specified in the manifest, may contain wildcards (* or ?) as qualifier value; + * if `null`, {NilQualifier} is used. * @param testee - * the qualifier to match the pattern; must not contain wildcards + * the qualifier to test against the capability qualifier; must not contain wildcards. */ -export function testQualifier(pattern: Qualifier, testee: Qualifier): boolean { - const _pattern = pattern || NilQualifier; +export function matchesCapabilityQualifier(capabilityQualifier: Qualifier, testee: Qualifier): boolean { + const _capabilityQualifier = capabilityQualifier || NilQualifier; const _testee = testee || NilQualifier; - if (!_pattern.hasOwnProperty('*') && Object.keys(_pattern).sort().join(',') !== Object.keys(_testee).sort().join(',')) { + if (_capabilityQualifier.hasOwnProperty('*')) { + throw Error(`[IllegalCapabilityKeyError] Capability qualifiers do not support \`*\` as key`); + } + + // Test if testee has all required entries + if (!Object.keys(_capabilityQualifier).every(key => _capabilityQualifier[key] === '?' || _testee.hasOwnProperty(key))) { return false; } - return Object.keys(_pattern) + // Test if testee has no additional entries + if (!Object.keys(_testee).every(key => _capabilityQualifier.hasOwnProperty(key))) { + return false; + } + + return Object.keys(_capabilityQualifier) + .every(key => { + if (_capabilityQualifier[key] === '*') { + return _testee[key] !== undefined && _testee[key] !== null; + } + if (_capabilityQualifier[key] === '?') { + return true; + } + return _capabilityQualifier[key] === _testee[key]; + }); +} + +/** + * Tests if the given qualifier matches the intent qualifier. + * + * @param intentQualifier + * qualifier as specified in the manifest, may contain wildcards (*) as qualifier key or/and wildcards (* or ?) as qualifier value; + * if `null`, {NilQualifier} is used. + * @param testee + * the qualifier to test against the intent qualifier; must not contain wildcards + */ +export function matchesIntentQualifier(intentQualifier: Qualifier, testee: Qualifier): boolean { + const _intentQualifier = intentQualifier || NilQualifier; + const _testee = testee || NilQualifier; + + // Test if testee has all required entries + if (!Object.keys(_intentQualifier).filter(key => key !== '*').every(key => _intentQualifier[key] === '?' || _testee.hasOwnProperty(key))) { + return false; + } + + // Test if testee has no additional entries + if (!_intentQualifier.hasOwnProperty('*') && !Object.keys(_testee).every(key => _intentQualifier.hasOwnProperty(key))) { + return false; + } + + return Object.keys(_intentQualifier) .filter(key => key !== '*') .every(key => { - if (_pattern[key] === '*') { + if (_intentQualifier[key] === '*') { return _testee[key] !== undefined && _testee[key] !== null; } - return _pattern[key] === _testee[key]; + if (_intentQualifier[key] === '?') { + return true; + } + return _intentQualifier[key] === _testee[key]; }); } diff --git a/projects/scion/workbench-application-platform/src/lib/manifest-capability/manifest-registry-intent-handler.service.ts b/projects/scion/workbench-application-platform/src/lib/manifest-capability/manifest-registry-intent-handler.service.ts index cc1c6cf63..0943eb6a5 100644 --- a/projects/scion/workbench-application-platform/src/lib/manifest-capability/manifest-registry-intent-handler.service.ts +++ b/projects/scion/workbench-application-platform/src/lib/manifest-capability/manifest-registry-intent-handler.service.ts @@ -15,7 +15,7 @@ import { MessageBus } from '../core/message-bus.service'; import { ApplicationRegistry } from '../core/application-registry.service'; import { ManifestRegistry } from '../core/manifest-registry.service'; import { Logger } from '../core/logger.service'; -import { testQualifier } from '../core/qualifier-tester'; +import { matchesIntentQualifier } from '../core/qualifier-tester'; /** * Allows to query manifest registry. @@ -88,8 +88,7 @@ export class ManifestRegistryIntentHandler implements IntentHandler { const intent: Intent = this._manifestRegistry.getIntent(intentId); const providers: Application[] = this._manifestRegistry.getCapabilities(intent.type, intent.qualifier) - .filter(capability => !capability.metadata.proxy) - .filter(capability => !capability.private || this._manifestRegistry.isScopeCheckDisabled(intent.metadata.symbolicAppName) || capability.metadata.symbolicAppName === intent.metadata.symbolicAppName) + .filter(capability => this._manifestRegistry.isVisibleForApplication(capability, intent.metadata.symbolicAppName)) .map(capability => this._applicationRegistry.getApplication(capability.metadata.symbolicAppName)); this._messageBus.publishReply(providers, envelope.sender, envelope.replyToUid); } @@ -106,7 +105,7 @@ export class ManifestRegistryIntentHandler implements IntentHandler { const intents = this._manifestRegistry.getIntentsByApplication(application.symbolicName); const isConsumer = intents .filter(intent => !capability.private || this._manifestRegistry.isScopeCheckDisabled(intent.metadata.symbolicAppName) || intent.metadata.symbolicAppName === capability.metadata.symbolicAppName) - .some(intent => intent.type === capability.type && testQualifier(capability.qualifier, intent.qualifier)); + .some(intent => intent.type === capability.type && matchesIntentQualifier(capability.qualifier, intent.qualifier)); if (isConsumer) { consumers.push(application); } @@ -125,8 +124,7 @@ export class ManifestRegistryIntentHandler implements IntentHandler { const qualifier: Qualifier = envelope.message.payload.qualifier; const capabilities: Capability[] = this._manifestRegistry.getCapabilities(type, qualifier) - .filter(capability => !capability.metadata.proxy) - .filter(capability => !capability.private || this._manifestRegistry.isScopeCheckDisabled(envelope.sender) || capability.metadata.symbolicAppName === envelope.sender) + .filter(capability => this._manifestRegistry.isVisibleForApplication(capability, envelope.sender)) .filter(capability => this._manifestRegistry.hasIntent(envelope.sender, capability.type, capability.qualifier)); this._messageBus.publishReply(capabilities, envelope.sender, envelope.replyToUid); } @@ -152,8 +150,7 @@ export class ManifestRegistryIntentHandler implements IntentHandler { scopeCheckDisabled: application.scopeCheckDisabled, restrictions: application.restrictions, intents: this._manifestRegistry.getIntentsByApplication(application.symbolicName), - capabilities: this._manifestRegistry.getCapabilitiesByApplication(application.symbolicName) - .filter(capability => !capability.metadata.proxy), + capabilities: this._manifestRegistry.getCapabilitiesByApplication(application.symbolicName), }; } } diff --git a/projects/scion/workbench-application-platform/src/lib/popup-capability/popup-capability.module.ts b/projects/scion/workbench-application-platform/src/lib/popup-capability/popup-capability.module.ts index fe60e7e7e..3069db816 100644 --- a/projects/scion/workbench-application-platform/src/lib/popup-capability/popup-capability.module.ts +++ b/projects/scion/workbench-application-platform/src/lib/popup-capability/popup-capability.module.ts @@ -8,13 +8,12 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { NgModule } from '@angular/core'; +import { APP_INITIALIZER, Injector, NgModule } from '@angular/core'; import { CoreModule } from '../core/core.module'; import { WorkbenchModule } from '@scion/workbench'; import { CommonModule } from '@angular/common'; import { PopupOutletComponent } from './popup-outlet.component'; -import { INTENT_HANDLER } from '../core/metadata'; -import { PopupIntentHandler } from './popup-intent-handler.service'; +import { PopupIntentDispatcher } from './popup-intent-dispatcher.service'; /** * Built-in capability to show a popup. @@ -29,7 +28,8 @@ import { PopupIntentHandler } from './popup-intent-handler.service'; WorkbenchModule.forChild(), ], providers: [ - {provide: INTENT_HANDLER, useClass: PopupIntentHandler, multi: true}, + {provide: APP_INITIALIZER, useFactory: provideModuleInitializerFn, multi: true, deps: [Injector]}, + PopupIntentDispatcher, ], entryComponents: [ PopupOutletComponent, @@ -37,3 +37,10 @@ import { PopupIntentHandler } from './popup-intent-handler.service'; }) export class PopupCapabilityModule { } + +export function provideModuleInitializerFn(injector: Injector): () => void { + // use injector because Angular Router cannot be injected in `APP_INITIALIZER` function + // do not return the function directly to not break the AOT build (add redundant assignment) + const fn = (): void => injector.get(PopupIntentDispatcher).init(); + return fn; +} diff --git a/projects/scion/workbench-application-platform/src/lib/popup-capability/popup-intent-handler.service.ts b/projects/scion/workbench-application-platform/src/lib/popup-capability/popup-intent-dispatcher.service.ts similarity index 74% rename from projects/scion/workbench-application-platform/src/lib/popup-capability/popup-intent-handler.service.ts rename to projects/scion/workbench-application-platform/src/lib/popup-capability/popup-intent-dispatcher.service.ts index 4db683955..1cd6a3083 100644 --- a/projects/scion/workbench-application-platform/src/lib/popup-capability/popup-intent-handler.service.ts +++ b/projects/scion/workbench-application-platform/src/lib/popup-capability/popup-intent-dispatcher.service.ts @@ -10,13 +10,15 @@ import { ElementRef, Inject, Injectable, OnDestroy, Renderer2, RendererFactory2 } from '@angular/core'; import { PopupConfig, PopupService } from '@scion/workbench'; -import { IntentHandler } from '../core/metadata'; import { ManifestRegistry } from '../core/manifest-registry.service'; import { Logger } from '../core/logger.service'; import { PopupInput, PopupOutletComponent } from './popup-outlet.component'; import { MessageBus } from '../core/message-bus.service'; import { DOCUMENT } from '@angular/common'; -import { AnyQualifier, IntentMessage, MessageEnvelope, PlatformCapabilityTypes, PopupCapability, PopupIntentMessage } from '@scion/workbench-application-platform.api'; +import { IntentMessage, MessageEnvelope, PlatformCapabilityTypes, PopupCapability, PopupIntentMessage } from '@scion/workbench-application-platform.api'; +import { filter, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { ManifestCollector } from '../core/manifest-collector.service'; /** * Shows a workbench popup for intents of the type 'popup'. @@ -27,13 +29,9 @@ import { AnyQualifier, IntentMessage, MessageEnvelope, PlatformCapabilityTypes, * is looked up to provide metadata about the page to load in the popup. */ @Injectable() -export class PopupIntentHandler implements IntentHandler, OnDestroy { - - public readonly type: PlatformCapabilityTypes = PlatformCapabilityTypes.Popup; - public readonly qualifier = AnyQualifier; - public readonly proxy = true; - public readonly description = 'Shows a workbench popup for capabilities of the type \'popup\'.'; +export class PopupIntentDispatcher implements OnDestroy { + private _destroy$ = new Subject<void>(); private _renderer: Renderer2; constructor(private _logger: Logger, @@ -41,11 +39,16 @@ export class PopupIntentHandler implements IntentHandler, OnDestroy { private _messageBus: MessageBus, private _popupService: PopupService, @Inject(DOCUMENT) private _document: any, - rendererFactory: RendererFactory2) { - this._renderer = rendererFactory.createRenderer(null, null); + rendererFactory: RendererFactory2, + private _manifestCollector: ManifestCollector) { + this._renderer = rendererFactory.createRenderer(null, null); + } + + public init(): void { + this._manifestCollector.whenManifests.then(() => this.installIntentListener()); } - public onIntent(envelope: MessageEnvelope<PopupIntentMessage>): void { + private onIntent(envelope: MessageEnvelope<PopupIntentMessage>): void { const intentMessage: PopupIntentMessage = envelope.message; const popupCapability = this.resolvePopupCapabilityProvider(envelope); if (!popupCapability) { @@ -77,6 +80,21 @@ export class PopupIntentHandler implements IntentHandler, OnDestroy { }); } + private installIntentListener(): void { + this._messageBus.receiveIntents$() + .pipe( + filter(envelope => envelope.message.type === PlatformCapabilityTypes.Popup), + takeUntil(this._destroy$), + ) + .subscribe((envelope: MessageEnvelope<PopupIntentMessage>) => { + try { + this.onIntent(envelope); + } catch (error) { + this._logger.error(`Failed to handle intent [${JSON.stringify(envelope.message.qualifier || {})}]`, error); + } + }); + } + private createVirtualAnchorElement(outletBoundingBox: ClientRect | null, anchor: ClientRect): Element { const outletTop = outletBoundingBox && outletBoundingBox.top || 0; const outletLeft = outletBoundingBox && outletBoundingBox.left || 0; @@ -95,15 +113,8 @@ export class PopupIntentHandler implements IntentHandler, OnDestroy { private resolvePopupCapabilityProvider(envelope: MessageEnvelope<IntentMessage>): PopupCapability { const qualifier = envelope.message.qualifier; - const popupCapabilities = this._manifestRegistry.getCapabilities<PopupCapability>(this.type, qualifier) - .filter(popupCapability => { - // Skip proxy providers (e.g. this implementor class) - return !popupCapability.metadata.proxy; - }) - .filter(popupCapability => { - // Skip if the capability has private visibility and the intending application does not provide the view capability itself - return !popupCapability.private || this._manifestRegistry.isScopeCheckDisabled(envelope.sender) || envelope.sender === popupCapability.metadata.symbolicAppName; - }); + const popupCapabilities = this._manifestRegistry.getCapabilities<PopupCapability>(PlatformCapabilityTypes.Popup, qualifier) + .filter(capability => this._manifestRegistry.isVisibleForApplication(capability, envelope.sender)); if (popupCapabilities.length === 0) { this._logger.error(`[IllegalStateError] No capability registered matching the qualifier '${JSON.stringify(qualifier || {})}'.`, popupCapabilities); @@ -119,5 +130,6 @@ export class PopupIntentHandler implements IntentHandler, OnDestroy { public ngOnDestroy(): void { this._renderer.destroy(); + this._destroy$.next(); } } diff --git a/projects/scion/workbench-application-platform/src/lib/spec/array.util.spec.ts b/projects/scion/workbench-application-platform/src/lib/spec/array.util.spec.ts new file mode 100644 index 000000000..5bbba36a0 --- /dev/null +++ b/projects/scion/workbench-application-platform/src/lib/spec/array.util.spec.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2018-2019 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import { Arrays } from '../core/array.util'; + +describe('Arrays', () => { + + describe('Arrays.equal', () => { + + it('should be equal for same array references', () => { + const array = ['a', 'b', 'c']; + expect(Arrays.equal(array, array)).toBeTruthy(); + }); + + it('should be equal for same elements (same order)', () => { + const array1 = ['a', 'b', 'c']; + const array2 = ['a', 'b', 'c']; + expect(Arrays.equal(array1, array2)).toBeTruthy(); + }); + + it('should be equal for same elements (unordered)', () => { + const array1 = ['a', 'b', 'c']; + const array2 = ['a', 'c', 'b']; + expect(Arrays.equal(array1, array2, false)).toBeTruthy(); + }); + + it('should not be equal for different elements (1)', () => { + const array1 = ['a', 'b', 'c']; + const array2 = ['a', 'b', 'c', 'e']; + expect(Arrays.equal(array1, array2)).toBeFalsy(); + }); + + it('should not be equal for different elements (2)', () => { + const array1 = ['a', 'b', 'c']; + const array2 = ['a', 'B', 'c']; + expect(Arrays.equal(array1, array2)).toBeFalsy(); + }); + + it('should not be equal if ordered differently', () => { + const array1 = ['a', 'b', 'c']; + const array2 = ['a', 'c', 'b']; + expect(Arrays.equal(array1, array2)).toBeFalsy(); + }); + + it('should be equal if ordered differently', () => { + const array1 = ['a', 'b', 'c']; + const array2 = ['a', 'c', 'b']; + expect(Arrays.equal(array1, array2, false)).toBeTruthy(); + }); + }); + + describe('Arrays.last', () => { + + it('should find the last item matching the predicate', () => { + const array = ['a', 'b', 'c', 'd', 'e']; + expect(Arrays.last(array, (item: string): boolean => item === 'c')).toEqual('c'); + }); + + it('should return `undefined` if no element matches the predicate', () => { + const array = ['a', 'b', 'c', 'd', 'e']; + expect(Arrays.last(array, () => false)).toBeUndefined(); + }); + + it('should return the last item in the array if no predicate is specified', () => { + const array = ['a', 'b', 'c', 'd', 'e']; + expect(Arrays.last(array)).toEqual('e'); + }); + + it('should return `undefined` if the array is empty', () => { + expect(Arrays.last([])).toBeUndefined(); + expect(Arrays.last([], () => true)).toBeUndefined(); + }); + }); + + describe('Arrays.remove', () => { + + it('should remove the specified element', () => { + const array = ['a', 'b', 'c', 'd', 'e']; + expect(Arrays.remove(array, 'c')).toEqual(['a', 'b', 'd', 'e']); + }); + + it('should not modify the original array', () => { + const array = ['a', 'b', 'c', 'd', 'e']; + expect(Arrays.remove(array, 'c')).toEqual(['a', 'b', 'd', 'e']); + expect(array).toEqual(['a', 'b', 'c', 'd', 'e']); + }); + }); +}); diff --git a/projects/scion/workbench-application-platform/src/lib/spec/manifest-registry.spec.ts b/projects/scion/workbench-application-platform/src/lib/spec/manifest-registry.spec.ts index 39d471cce..5183801c8 100644 --- a/projects/scion/workbench-application-platform/src/lib/spec/manifest-registry.spec.ts +++ b/projects/scion/workbench-application-platform/src/lib/spec/manifest-registry.spec.ts @@ -42,15 +42,6 @@ describe('ManifestRegistry', () => { expect(manifestRegistry.isHandled('app-2', type, qualifier)).toBeFalsy(); expect(manifestRegistry.isHandled('app-3', type, qualifier)).toBeTruthy(); }))); - - it('should hide proxy capability providers from any application', fakeAsync(inject([ManifestRegistry], (manifestRegistry: ManifestRegistry) => { - const type = PlatformCapabilityTypes.View; - const qualifier: Qualifier = {entity: 'entity'}; - manifestRegistry.registerCapability('app-1', [{type, qualifier, private: false}], true); - - expect(manifestRegistry.isHandled('app-1', type, qualifier)).toBeFalsy(); - expect(manifestRegistry.isHandled('app-2', type, qualifier)).toBeFalsy(); - }))); }); describe('function \'hasCapability(...)\'', () => { @@ -61,7 +52,7 @@ describe('ManifestRegistry', () => { const type3 = 'type-3'; const qualifier1: Qualifier = {entity: 'entity', qualifier: 1}; const qualifier2: Qualifier = {entity: 'entity', qualifier: 2}; - const qualifier3: Qualifier = {'*': '*'}; + const qualifier3: Qualifier = {entity: '?'}; manifestRegistry.registerCapability('app-1', [{type: type1, qualifier: qualifier1, private: false}]); manifestRegistry.registerCapability('app-1', [{type: type2, qualifier: qualifier2, private: true}]); manifestRegistry.registerCapability('app-1', [{type: type3, qualifier: qualifier3, private: false}]); @@ -69,8 +60,6 @@ describe('ManifestRegistry', () => { expect(manifestRegistry.hasCapability('app-1', type1, qualifier1)).toBeTruthy(); expect(manifestRegistry.hasCapability('app-1', type2, qualifier2)).toBeTruthy(); expect(manifestRegistry.hasCapability('app-1', type3, {})).toBeTruthy(); - expect(manifestRegistry.hasCapability('app-1', type3, {}, () => true)).toBeTruthy(); - expect(manifestRegistry.hasCapability('app-1', type3, {}, () => false)).toBeFalsy(); expect(manifestRegistry.hasCapability('app-1', 'other-type', {})).toBeFalsy(); expect(manifestRegistry.hasCapability('app-2', type1, qualifier1)).toBeFalsy(); @@ -91,7 +80,7 @@ describe('ManifestRegistry', () => { expect(manifestRegistry.hasCapability('app-2', type, undefined)).toBeFalsy(); }))); - it('should return `true` if a qualifier matches the qualifier pattern', fakeAsync(inject([ManifestRegistry], (manifestRegistry: ManifestRegistry) => { + it('should return `true` if a qualifier matches the qualifier pattern (*)', fakeAsync(inject([ManifestRegistry], (manifestRegistry: ManifestRegistry) => { const type = 'type'; manifestRegistry.registerCapability('app-1', [{type, qualifier: {entity: '*'}, private: false}]); @@ -105,6 +94,29 @@ describe('ManifestRegistry', () => { expect(manifestRegistry.hasCapability('app-2', type, {entity: 'entity-2'})).toBeFalsy(); expect(manifestRegistry.hasCapability('app-2', type, {entity: 'entity-2', name: 'smith'})).toBeFalsy(); }))); + + it('should return `true` if a qualifier matches the qualifier pattern (?)', fakeAsync(inject([ManifestRegistry], (manifestRegistry: ManifestRegistry) => { + const type = 'type'; + manifestRegistry.registerCapability('app-1', [{type, qualifier: {entity: '?'}, private: false}]); + + expect(manifestRegistry.hasCapability('app-1', type, null)).toBeTruthy(); + expect(manifestRegistry.hasCapability('app-1', type, undefined)).toBeTruthy(); + expect(manifestRegistry.hasCapability('app-1', type, {})).toBeTruthy(); + expect(manifestRegistry.hasCapability('app-1', type, {entity: null})).toBeTruthy(); + expect(manifestRegistry.hasCapability('app-1', type, {entity: undefined})).toBeTruthy(); + expect(manifestRegistry.hasCapability('app-1', type, {entity: 'optional-entity-1'})).toBeTruthy(); + expect(manifestRegistry.hasCapability('app-1', type, {entity: 'optional-entity-2'})).toBeTruthy(); + expect(manifestRegistry.hasCapability('app-1', type, {entity: 'optional-entity-2', name: 'smith'})).toBeFalsy(); + + expect(manifestRegistry.hasCapability('app-2', type, null)).toBeFalsy(); + expect(manifestRegistry.hasCapability('app-2', type, undefined)).toBeFalsy(); + expect(manifestRegistry.hasCapability('app-2', type, {})).toBeFalsy(); + expect(manifestRegistry.hasCapability('app-2', type, {entity: null})).toBeFalsy(); + expect(manifestRegistry.hasCapability('app-2', type, {entity: undefined})).toBeFalsy(); + expect(manifestRegistry.hasCapability('app-2', type, {entity: 'optional-entity-1'})).toBeFalsy(); + expect(manifestRegistry.hasCapability('app-2', type, {entity: 'optional-entity-2'})).toBeFalsy(); + expect(manifestRegistry.hasCapability('app-2', type, {entity: 'optional-entity-2', name: 'smith'})).toBeFalsy(); + }))); }); describe('function \'hasIntent(...)\'', () => { @@ -167,7 +179,7 @@ describe('ManifestRegistry', () => { expect(manifestRegistry.hasIntent('app-2', type, undefined)).toBeTruthy(); }))); - it('should return `false` if `null` or `undefined` is given as wildcard intent value', fakeAsync(inject([ManifestRegistry], (manifestRegistry: ManifestRegistry) => { + it('should return `false` if `null` or `undefined` is given as wildcard (*) intent value', fakeAsync(inject([ManifestRegistry], (manifestRegistry: ManifestRegistry) => { const type = 'type'; manifestRegistry.registerIntents('app-2', [{type, qualifier: {q: '*'}}]); @@ -175,6 +187,16 @@ describe('ManifestRegistry', () => { expect(manifestRegistry.hasIntent('app-2', type, {q: undefined})).toBeFalsy(); }))); + it('should return `true` if `null` or `undefined` is given as wildcard (?) intent value', fakeAsync(inject([ManifestRegistry], (manifestRegistry: ManifestRegistry) => { + const type = 'type'; + manifestRegistry.registerIntents('app-2', [{type, qualifier: {q: '?'}}]); + + expect(manifestRegistry.hasIntent('app-2', type, {q: null})).toBeTruthy(); + expect(manifestRegistry.hasIntent('app-2', type, {q: undefined})).toBeTruthy(); + expect(manifestRegistry.hasIntent('app-2', type, {})).toBeTruthy(); + expect(manifestRegistry.hasIntent('app-2', type, null)).toBeTruthy(); + expect(manifestRegistry.hasIntent('app-2', type, undefined)).toBeTruthy(); + }))); }); describe('function \'getCapabilities(...)\'', () => { @@ -183,6 +205,7 @@ describe('ManifestRegistry', () => { manifestRegistry.registerCapability('app-1', [{type: 'type-1'}]); manifestRegistry.registerCapability('app-1', [{type: 'type-2', qualifier: {entity: 'entity'}}]); manifestRegistry.registerCapability('app-1', [{type: 'type-3', qualifier: {entity: '*'}}]); + manifestRegistry.registerCapability('app-1', [{type: 'type-4', qualifier: {entity: '?'}}]); expect(manifestRegistry.getCapabilities('type-1', null).length).toBe(1); expect(manifestRegistry.getCapabilities('type-1', {}).length).toBe(1); @@ -200,6 +223,13 @@ describe('ManifestRegistry', () => { expect(manifestRegistry.getCapabilities('type-3', {'entity': null}).length).toBe(0); expect(manifestRegistry.getCapabilities('type-3', {'entity': 'other-entity'}).length).toBe(1); expect(manifestRegistry.getCapabilities('type-3', {'some': 'qualifier'}).length).toBe(0); + + expect(manifestRegistry.getCapabilities('type-4', null).length).toBe(1); + expect(manifestRegistry.getCapabilities('type-4', {}).length).toBe(1); + expect(manifestRegistry.getCapabilities('type-4', {'entity': 'entity'}).length).toBe(1); + expect(manifestRegistry.getCapabilities('type-4', {'entity': null}).length).toBe(1); + expect(manifestRegistry.getCapabilities('type-4', {'entity': 'other-entity'}).length).toBe(1); + expect(manifestRegistry.getCapabilities('type-4', {'some': 'qualifier'}).length).toBe(0); }))); }); @@ -219,6 +249,21 @@ describe('ManifestRegistry', () => { }))); }); + describe('function \'getCapabilitiesByPredicate(...)\'', () => { + + it('should return capabilities matching the given predicate', fakeAsync(inject([ManifestRegistry], (manifestRegistry: ManifestRegistry) => { + manifestRegistry.registerCapability('app-1', [{type: 'type-1', private: false}]); + manifestRegistry.registerCapability('app-1', [{type: 'type-2', qualifier: {entity: 'entity'}, private: false}]); + manifestRegistry.registerCapability('app-1', [{type: 'type-3', qualifier: {entity: '*'}, private: false}]); + manifestRegistry.registerCapability('app-1', [{type: 'type-4', qualifier: {entity: '?'}, private: true}]); + + expect(manifestRegistry.getCapabilitiesByPredicate(() => true).length).toBe(4); + expect(manifestRegistry.getCapabilitiesByPredicate(capability => !capability.private).length).toBe(3); + expect(manifestRegistry.getCapabilitiesByPredicate(capability => capability.type === 'type-3').length).toBe(1); + expect(manifestRegistry.getCapabilitiesByPredicate(capability => capability.qualifier && capability.qualifier.hasOwnProperty('entity')).length).toBe(3); + }))); + }); + describe('function \'getIntentsByApplication(...)\'', () => { it('should return intents by application', fakeAsync(inject([ManifestRegistry], (manifestRegistry: ManifestRegistry) => { diff --git a/projects/scion/workbench-application-platform/src/lib/spec/message-bus.spec.ts b/projects/scion/workbench-application-platform/src/lib/spec/message-bus.spec.ts index 35c502b65..671113367 100644 --- a/projects/scion/workbench-application-platform/src/lib/spec/message-bus.spec.ts +++ b/projects/scion/workbench-application-platform/src/lib/spec/message-bus.spec.ts @@ -213,13 +213,13 @@ describe('MessageBus', () => { name: 'app-1', capability: [{type: PlatformCapabilityTypes.View, private: false}], }); registerApp({ - name: 'app-2', capability: [{type: PlatformCapabilityTypes.View, qualifier: {'*': '*'}, private: false}], + name: 'app-2', capability: [{type: PlatformCapabilityTypes.View, qualifier: {entity: '*', viewType: '*'}, private: false}], }); registerApp({ - name: 'app-3', capability: [{type: PlatformCapabilityTypes.View, qualifier: {entity: 'person', '*': '*'}, private: false}], + name: 'app-3', capability: [{type: PlatformCapabilityTypes.View, qualifier: {entity: '?', viewType: '?'}, private: false}], }); registerApp({ - name: 'app-4', capability: [{type: PlatformCapabilityTypes.View, qualifier: {entity: 'company', '*': '*'}, private: false}], + name: 'app-4', capability: [{type: PlatformCapabilityTypes.View, qualifier: {entity: 'company', viewType: 'company-info'}, private: false}], }); registerApp({ name: 'app-5', capability: [{type: PlatformCapabilityTypes.View, qualifier: {entity: 'person', viewType: '*'}, private: false}], @@ -265,7 +265,7 @@ describe('MessageBus', () => { expect(messageDispatched).toBeTruthy(); }))); - it('should be published if the publishing app manifests a matching wildcard capability', fakeAsync(inject([MessageBus], (messageBus: MessageBus) => { + it('should be published if the publishing app manifests a matching wildcard (*) capability', fakeAsync(inject([MessageBus], (messageBus: MessageBus) => { registerApp({ name: 'app-1', capability: [{type: PlatformCapabilityTypes.View, qualifier: {entity: 'person', viewType: '*'}, private: false}], }); @@ -275,6 +275,16 @@ describe('MessageBus', () => { expect(messageDispatched).toBeTruthy(); }))); + it('should be published if the publishing app manifests a matching wildcard (?) capability', fakeAsync(inject([MessageBus], (messageBus: MessageBus) => { + registerApp({ + name: 'app-1', capability: [{type: PlatformCapabilityTypes.View, qualifier: {entity: 'person', viewType: '?'}, private: false}], + }); + + const envelope = createCapabilityEnvelope({type: PlatformCapabilityTypes.View, qualifier: {entity: 'person'}}); + messageBus.publishMessageIfQualified(envelope, 'app-1'); + expect(messageDispatched).toBeTruthy(); + }))); + it('should not be published if the publishing app does not manifest a respective capability [NotQualifiedError]', fakeAsync(inject([MessageBus], (messageBus: MessageBus) => { registerApp({ name: 'app-1', capability: [{type: PlatformCapabilityTypes.View, qualifier: {entity: 'person', viewType: 'personal-info'}, private: false}], diff --git a/projects/scion/workbench-application-platform/src/lib/spec/qualifier.spec.ts b/projects/scion/workbench-application-platform/src/lib/spec/qualifier.spec.ts index c41cb2478..58cc43dca 100644 --- a/projects/scion/workbench-application-platform/src/lib/spec/qualifier.spec.ts +++ b/projects/scion/workbench-application-platform/src/lib/spec/qualifier.spec.ts @@ -8,64 +8,149 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { testQualifier } from '../core/qualifier-tester'; +import { matchesCapabilityQualifier, matchesIntentQualifier } from '../core/qualifier-tester'; describe('Qualifier', () => { - describe('function \'testQualifier(...)\'', () => { + describe('function \'matchesCapabilityQualifier(...)\'', () => { it('tests strict equality', () => { - expect(testQualifier({entity: 'person', id: 42}, {entity: 'person', id: 42})).toBeTruthy(); - expect(testQualifier({entity: 'person', id: 42}, {entity: 'person', id: '42'})).toBeFalsy(); - expect(testQualifier({entity: 'person', id: 42}, {entity: 'person', id: 43})).toBeFalsy(); - expect(testQualifier({entity: 'person', id: 42}, {entity: 'company', id: 42})).toBeFalsy(); - expect(testQualifier({entity: 'person', id: 42}, {entity: 'person'})).toBeFalsy(); - expect(testQualifier({entity: 'person', id: 42}, {entity: 'person', id: 42, name: 'smith'})).toBeFalsy(); - expect(testQualifier({entity: 'person', id: 42}, null)).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: 42}, {entity: 'person', id: 42})).toBeTruthy(); + expect(matchesCapabilityQualifier({entity: 'person', id: 42}, {entity: 'person', id: '42'})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: 42}, {entity: 'person', id: 43})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: 42}, {entity: 'company', id: 42})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: 42}, {entity: 'person'})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: 42}, {entity: 'person', id: 42, name: 'smith'})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: 42}, null)).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', flag: true}, {entity: 'person', flag: true})).toBeTruthy(); + expect(matchesCapabilityQualifier({entity: 'person', flag: true}, {entity: 'person', flag: 'true'})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', flag: true}, {entity: 'person', flag: false})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', flag: false}, {entity: 'person', flag: 'false'})).toBeFalsy(); }); - it('supports value wildcards', () => { - expect(testQualifier({entity: 'person', id: '*'}, {entity: 'person', id: 42})).toBeTruthy(); - expect(testQualifier({entity: 'person', id: '*'}, {entity: 'person', id: '42'})).toBeTruthy(); - expect(testQualifier({entity: 'person', id: '*'}, {entity: 'person', id: 43})).toBeTruthy(); - expect(testQualifier({entity: 'person', id: '*'}, {entity: 'company', id: 42})).toBeFalsy(); - expect(testQualifier({entity: 'person', id: '*'}, {entity: 'person'})).toBeFalsy(); - expect(testQualifier({entity: 'person', id: '*'}, {entity: 'person', id: 42, name: 'smith'})).toBeFalsy(); - expect(testQualifier({entity: 'person', id: '*'}, null)).toBeFalsy(); - - expect(testQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', b: 'b', c: 'c'})).toBeTruthy(); - expect(testQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', b: null, c: 'c'})).toBeFalsy(); - expect(testQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', c: 'c'})).toBeFalsy(); - expect(testQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', c: 'c'})).toBeFalsy(); - expect(testQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', c: 'c', d: 'd'})).toBeFalsy(); + it('supports value wildcards (*)', () => { + expect(matchesCapabilityQualifier({entity: 'person', id: '*'}, {entity: 'person', id: 42})).toBeTruthy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '*'}, {entity: 'person', id: '42'})).toBeTruthy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '*'}, {entity: 'person', id: 43})).toBeTruthy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '*'}, {entity: 'company', id: 42})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '*'}, {entity: 'person'})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '*'}, {entity: 'person', id: 42, name: 'smith'})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '*'}, null)).toBeFalsy(); + + expect(matchesCapabilityQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', b: 'b', c: 'c'})).toBeTruthy(); + expect(matchesCapabilityQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', b: null, c: 'c'})).toBeFalsy(); + expect(matchesCapabilityQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', c: 'c'})).toBeFalsy(); + expect(matchesCapabilityQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', c: 'c', d: 'd'})).toBeFalsy(); + }); + + it('supports value wildcards (?)', () => { + expect(matchesCapabilityQualifier({entity: 'person', id: '?'}, {entity: 'person', id: 42})).toBeTruthy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '?'}, {entity: 'person', id: '42'})).toBeTruthy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '?'}, {entity: 'person', id: 43})).toBeTruthy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '?'}, {entity: 'company', id: 42})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '?'}, {entity: 'person'})).toBeTruthy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '?'}, {entity: 'person', id: 42, name: 'smith'})).toBeFalsy(); + expect(matchesCapabilityQualifier({entity: 'person', id: '?'}, null)).toBeFalsy(); + + expect(matchesCapabilityQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', b: 'b', c: 'c'})).toBeTruthy(); + expect(matchesCapabilityQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', b: null, c: 'c'})).toBeTruthy(); + expect(matchesCapabilityQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', c: 'c'})).toBeTruthy(); + expect(matchesCapabilityQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', c: 'c', d: 'd'})).toBeFalsy(); + }); + + it('accepts only empty qualifiers', () => { + expect(matchesCapabilityQualifier({}, {})).toBeTruthy(); + expect(matchesCapabilityQualifier({}, null)).toBeTruthy(); + expect(matchesCapabilityQualifier({}, {entity: 'person'})).toBeFalsy(); + }); + + it('throws IllegalKeyError if qualifier key is \'*\'', () => { + const errorMatcher = /IllegalCapabilityKeyError/; + expect(() => matchesCapabilityQualifier({'*': '*'}, {})).toThrowError(errorMatcher); + expect(() => matchesCapabilityQualifier({'*': '*'}, null)).toThrowError(errorMatcher); + expect(() => matchesCapabilityQualifier({'*': '*'}, {entity: 'person'})).toThrowError(errorMatcher); + }); + + it('accepts only empty qualifiers if not providing a pattern qualifier', () => { + expect(matchesCapabilityQualifier(null, {})).toBeTruthy(); + expect(matchesCapabilityQualifier(null, null)).toBeTruthy(); + expect(matchesCapabilityQualifier(null, {entity: 'person'})).toBeFalsy(); + }); + }); + + describe('function \'matchesIntentQualifier(...)\'', () => { + + it('tests strict equality', () => { + expect(matchesIntentQualifier({entity: 'person', id: 42}, {entity: 'person', id: 42})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', id: 42}, {entity: 'person', id: '42'})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: 42}, {entity: 'person', id: 43})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: 42}, {entity: 'company', id: 42})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: 42}, {entity: 'person'})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: 42}, {entity: 'person', id: 42, name: 'smith'})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: 42}, null)).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', flag: true}, {entity: 'person', flag: true})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', flag: true}, {entity: 'person', flag: 'true'})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', flag: true}, {entity: 'person', flag: false})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', flag: false}, {entity: 'person', flag: 'false'})).toBeFalsy(); + }); + + it('supports value wildcards (*)', () => { + expect(matchesIntentQualifier({entity: 'person', id: '*'}, {entity: 'person', id: 42})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', id: '*'}, {entity: 'person', id: '42'})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', id: '*'}, {entity: 'person', id: 43})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', id: '*'}, {entity: 'company', id: 42})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: '*'}, {entity: 'person'})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: '*'}, {entity: 'person', id: 42, name: 'smith'})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: '*'}, null)).toBeFalsy(); + + expect(matchesIntentQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', b: 'b', c: 'c'})).toBeTruthy(); + expect(matchesIntentQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', b: null, c: 'c'})).toBeFalsy(); + expect(matchesIntentQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', c: 'c'})).toBeFalsy(); + expect(matchesIntentQualifier({a: 'a', b: '*', c: 'c'}, {a: 'a', c: 'c', d: 'd'})).toBeFalsy(); + }); + + it('supports value wildcards (?)', () => { + expect(matchesIntentQualifier({entity: 'person', id: '?'}, {entity: 'person', id: 42})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', id: '?'}, {entity: 'person', id: '42'})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', id: '?'}, {entity: 'person', id: 43})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', id: '?'}, {entity: 'company', id: 42})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: '?'}, {entity: 'person'})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', id: '?'}, {entity: 'person', id: 42, name: 'smith'})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', id: '?'}, null)).toBeFalsy(); + + expect(matchesIntentQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', b: 'b', c: 'c'})).toBeTruthy(); + expect(matchesIntentQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', b: null, c: 'c'})).toBeTruthy(); + expect(matchesIntentQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', b: undefined, c: 'c'})).toBeTruthy(); + expect(matchesIntentQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', c: 'c'})).toBeTruthy(); + expect(matchesIntentQualifier({a: 'a', b: '?', c: 'c'}, {a: 'a', c: 'c', d: 'd'})).toBeFalsy(); }); - it('supports key wildcards', () => { - expect(testQualifier({entity: 'person', '*': '*'}, {entity: 'person', id: 42})).toBeTruthy(); - expect(testQualifier({entity: 'person', '*': '*'}, {entity: 'person', id: '42'})).toBeTruthy(); - expect(testQualifier({entity: 'person', '*': '*'}, {entity: 'person', id: 43})).toBeTruthy(); - expect(testQualifier({entity: 'person', '*': '*'}, {entity: 'company', id: 42})).toBeFalsy(); - expect(testQualifier({entity: 'person', '*': '*'}, {entity: 'person'})).toBeTruthy(); - expect(testQualifier({entity: 'person', '*': '*'}, {entity: 'person', id: 42, name: 'smith'})).toBeTruthy(); - expect(testQualifier({entity: 'person', '*': '*'}, null)).toBeFalsy(); + it('supports key wildcards (*)', () => { + expect(matchesIntentQualifier({entity: 'person', '*': '*'}, {entity: 'person', id: 42})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', '*': '*'}, {entity: 'person', id: '42'})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', '*': '*'}, {entity: 'person', id: 43})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', '*': '*'}, {entity: 'company', id: 42})).toBeFalsy(); + expect(matchesIntentQualifier({entity: 'person', '*': '*'}, {entity: 'person'})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', '*': '*'}, {entity: 'person', id: 42, name: 'smith'})).toBeTruthy(); + expect(matchesIntentQualifier({entity: 'person', '*': '*'}, null)).toBeFalsy(); }); it('accepts only empty qualifiers', () => { - expect(testQualifier({}, {})).toBeTruthy(); - expect(testQualifier({}, null)).toBeTruthy(); - expect(testQualifier({}, {entity: 'person'})).toBeFalsy(); + expect(matchesIntentQualifier({}, {})).toBeTruthy(); + expect(matchesIntentQualifier({}, null)).toBeTruthy(); + expect(matchesIntentQualifier({}, {entity: 'person'})).toBeFalsy(); }); - it('accepts any qualifier is providing a wildcard qualifier', () => { - expect(testQualifier({'*': '*'}, {})).toBeTruthy(); - expect(testQualifier({'*': '*'}, null)).toBeTruthy(); - expect(testQualifier({'*': '*'}, {entity: 'person'})).toBeTruthy(); + it('accepts any qualifier if providing a wildcard qualifier', () => { + expect(matchesIntentQualifier({'*': '*'}, {})).toBeTruthy(); + expect(matchesIntentQualifier({'*': '*'}, null)).toBeTruthy(); + expect(matchesIntentQualifier({'*': '*'}, {entity: 'person'})).toBeTruthy(); }); it('accepts only empty qualifiers if not providing a pattern qualifier', () => { - expect(testQualifier(null, {})).toBeTruthy(); - expect(testQualifier(null, null)).toBeTruthy(); - expect(testQualifier(null, {entity: 'person'})).toBeFalsy(); + expect(matchesIntentQualifier(null, {})).toBeTruthy(); + expect(matchesIntentQualifier(null, null)).toBeTruthy(); + expect(matchesIntentQualifier(null, {entity: 'person'})).toBeFalsy(); }); }); }); diff --git a/projects/scion/workbench-application-platform/src/lib/view-capability/view-capability.module.ts b/projects/scion/workbench-application-platform/src/lib/view-capability/view-capability.module.ts index 4f75024eb..24d016a68 100644 --- a/projects/scion/workbench-application-platform/src/lib/view-capability/view-capability.module.ts +++ b/projects/scion/workbench-application-platform/src/lib/view-capability/view-capability.module.ts @@ -8,14 +8,13 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { NgModule } from '@angular/core'; +import { APP_INITIALIZER, Injector, NgModule } from '@angular/core'; import { CoreModule } from '../core/core.module'; -import { ViewIntentHandler } from './view-intent-handler.service'; +import { ViewIntentDispatcher } from './view-intent-dispatcher.service'; import { WorkbenchModule } from '@scion/workbench'; import { RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; import { ViewOutletComponent } from './view-outlet.component'; -import { INTENT_HANDLER } from '../core/metadata'; /** * Built-in capability to show a view. @@ -31,7 +30,8 @@ import { INTENT_HANDLER } from '../core/metadata'; RouterModule.forChild([]), ], providers: [ - {provide: INTENT_HANDLER, useClass: ViewIntentHandler, multi: true}, + {provide: APP_INITIALIZER, useFactory: provideModuleInitializerFn, multi: true, deps: [Injector]}, + ViewIntentDispatcher, ], entryComponents: [ ViewOutletComponent, @@ -39,3 +39,10 @@ import { INTENT_HANDLER } from '../core/metadata'; }) export class ViewCapabilityModule { } + +export function provideModuleInitializerFn(injector: Injector): () => void { + // use injector because Angular Router cannot be injected in `APP_INITIALIZER` function + // do not return the function directly to not break the AOT build (add redundant assignment) + const fn = (): void => injector.get(ViewIntentDispatcher).init(); + return fn; +} diff --git a/projects/scion/workbench-application-platform/src/lib/view-capability/view-intent-handler.service.ts b/projects/scion/workbench-application-platform/src/lib/view-capability/view-intent-dispatcher.service.ts similarity index 71% rename from projects/scion/workbench-application-platform/src/lib/view-capability/view-intent-handler.service.ts rename to projects/scion/workbench-application-platform/src/lib/view-capability/view-intent-dispatcher.service.ts index 7d31f19b1..1a42104fd 100644 --- a/projects/scion/workbench-application-platform/src/lib/view-capability/view-intent-handler.service.ts +++ b/projects/scion/workbench-application-platform/src/lib/view-capability/view-intent-dispatcher.service.ts @@ -8,17 +8,20 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { Injectable, Type } from '@angular/core'; +import { Injectable, OnDestroy, Type } from '@angular/core'; import { VIEW_CAPABILITY_ID_PARAM, VIEW_PATH_PARAM } from './metadata'; import { Router } from '@angular/router'; import { ViewOutletComponent } from './view-outlet.component'; import { WbNavigationExtras, WorkbenchActivityPartService, WorkbenchAuxiliaryRoutesRegistrator, WorkbenchRouter, WorkbenchService, WorkbenchView } from '@scion/workbench'; -import { noop } from 'rxjs'; -import { IntentHandler } from '../core/metadata'; +import { noop, Subject } from 'rxjs'; import { ApplicationRegistry } from '../core/application-registry.service'; import { ManifestRegistry } from '../core/manifest-registry.service'; import { Url } from '../core/url.util'; -import { AnyQualifier, MessageEnvelope, PlatformCapabilityTypes, Qualifier, ViewCapability, ViewIntentMessage } from '@scion/workbench-application-platform.api'; +import { MessageEnvelope, PlatformCapabilityTypes, Qualifier, ViewCapability, ViewIntentMessage } from '@scion/workbench-application-platform.api'; +import { filter, takeUntil } from 'rxjs/operators'; +import { MessageBus } from '../core/message-bus.service'; +import { Logger } from '../core/logger.service'; +import { ManifestCollector } from '../core/manifest-collector.service'; /** * Opens a workbench view for intents of the type 'view'. @@ -29,40 +32,33 @@ import { AnyQualifier, MessageEnvelope, PlatformCapabilityTypes, Qualifier, View * is looked up to provide metadata about the page to navigate to. */ @Injectable() -export class ViewIntentHandler implements IntentHandler { +export class ViewIntentDispatcher implements OnDestroy { - public readonly type: PlatformCapabilityTypes = PlatformCapabilityTypes.View; - public readonly qualifier = AnyQualifier; - public readonly proxy = true; - public readonly description = 'Open a workbench view for capabilities of the type \'view\'.'; - - private _applicationRegistry: ApplicationRegistry; - private _manifestRegistry: ManifestRegistry; + private _destroy$ = new Subject<void>(); constructor(private _workbench: WorkbenchService, private _router: Router, private _wbRouter: WorkbenchRouter, private _routesRegistrator: WorkbenchAuxiliaryRoutesRegistrator, - private _activityPartService: WorkbenchActivityPartService) { + private _activityPartService: WorkbenchActivityPartService, + private _applicationRegistry: ApplicationRegistry, + private _manifestRegistry: ManifestRegistry, + private _messageBus: MessageBus, + private _logger: Logger, + private _manifestCollector: ManifestCollector) { } - public onInit(applicationRegistry: ApplicationRegistry, manifestRegistry: ManifestRegistry): void { - this._applicationRegistry = applicationRegistry; - this._manifestRegistry = manifestRegistry; - this.installViewCapabilityRoutes(); - this.addToActivityPanel(); + public init(): void { + this._manifestCollector.whenManifests.then(() => { + this.installViewCapabilityRoutes(); + this.installIntentListener(); + this.addToActivityPanel(); + }); } - public onIntent(envelope: MessageEnvelope<ViewIntentMessage>): void { - this._manifestRegistry.getCapabilities<ViewCapability>(this.type, envelope.message.qualifier) - .filter(viewCapability => { - // Skip proxy providers (e.g. this implementor class) - return !viewCapability.metadata.proxy; - }) - .filter(viewCapability => { - // Skip if the capability has private visibility and the intending application does not provide the view capability itself - return !viewCapability.private || this._manifestRegistry.isScopeCheckDisabled(envelope.sender) || envelope.sender === viewCapability.metadata.symbolicAppName; - }) + private onIntent(envelope: MessageEnvelope<ViewIntentMessage>): void { + this._manifestRegistry.getCapabilities<ViewCapability>(PlatformCapabilityTypes.View, envelope.message.qualifier) + .filter(capability => this._manifestRegistry.isVisibleForApplication(capability, envelope.sender)) .forEach((viewCapability: ViewCapability) => { const intentMessage: ViewIntentMessage = envelope.message; const view = envelope._injector.get(WorkbenchView as Type<WorkbenchView>, null); // TODO [Angular 9]: remove type cast for abstract symbols once 'angular/issues/29905' and 'angular/issues/23611' are fixed @@ -86,6 +82,21 @@ export class ViewIntentHandler implements IntentHandler { }); } + private installIntentListener(): void { + this._messageBus.receiveIntents$() + .pipe( + filter(envelope => envelope.message.type === PlatformCapabilityTypes.View), + takeUntil(this._destroy$), + ) + .subscribe((envelope: MessageEnvelope<ViewIntentMessage>) => { + try { + this.onIntent(envelope); + } catch (error) { + this._logger.error(`Failed to handle intent [${JSON.stringify(envelope.message.qualifier || {})}]`, error); + } + }); + } + private installViewCapabilityRoutes(): void { // Register capability routes as primary routes. Auxiliary routes are registered when the views are opened (by the workbench) this._routesRegistrator.replaceRouterConfig([ @@ -100,8 +111,7 @@ export class ViewIntentHandler implements IntentHandler { } private addToActivityPanel(): void { - this._manifestRegistry.getCapabilitiesByType<ViewCapability>(this.type) - .filter(viewCapability => !viewCapability.metadata.proxy) + this._manifestRegistry.getCapabilitiesByType<ViewCapability>(PlatformCapabilityTypes.View) .filter(viewCapability => viewCapability.properties.activityItem) .forEach((viewCapability: ViewCapability) => { const activityItem = viewCapability.properties.activityItem; @@ -133,5 +143,9 @@ export class ViewIntentHandler implements IntentHandler { ...(matrixParamObject ? [matrixParamObject] : []), ]; } + + public ngOnDestroy(): void { + this._destroy$.next(); + } } diff --git a/projects/scion/workbench-application.core/src/lib/manifest-registry.service.ts b/projects/scion/workbench-application.core/src/lib/manifest-registry.service.ts index d1241fc86..dd142e78a 100644 --- a/projects/scion/workbench-application.core/src/lib/manifest-registry.service.ts +++ b/projects/scion/workbench-application.core/src/lib/manifest-registry.service.ts @@ -106,7 +106,7 @@ export class ManifestRegistryService implements Service { /** * Queries the manifest registry for capabilities of given type and qualifier. * - * There are ony capabilities returned for which the requesting application has manifested an intent. + * There are only capabilities returned for which the requesting application has manifested an intent. */ public capabilities$(type: string, qualifier: Qualifier): Observable<Capability[]> { const intentMessage: ManifestRegistryIntentMessages.FindCapabilities = { diff --git a/resources/site/getting-started/workbench-application-platform/getting-started.md b/resources/site/getting-started/workbench-application-platform/getting-started.md index 09a81cdda..f3462a9a3 100644 --- a/resources/site/getting-started/workbench-application-platform/getting-started.md +++ b/resources/site/getting-started/workbench-application-platform/getting-started.md @@ -49,10 +49,13 @@ To integrate an application, it should provide one or more entry points. When on The platform provides the concept of an application manifest to facilitate this entry-point invocation paradigm. Thus every application has a manifest which lists its capabilities and intents. Typically, the manifest is deployed as part of the application. #### Capability -A capability represents a feature which an application provides. It is of a specific type and has assigned a qualifier. The qualifier is used for logical addressing so that other applications can invoke it without knowing the provider nor the relevant entry point, if any. A qualifier is a dictionary of key-value pairs. It is allowed to use the wildcard character (*) as qualifier value or as qualifier key. +A capability represents a feature which an application provides. It is of a specific type and has assigned a qualifier. The qualifier is used for logical addressing so that other applications can invoke it without knowing the provider nor the relevant entry point, if any. A qualifier is a dictionary of key-value pairs. It is allowed to use the wildcard characters (\*) and (?) as qualifier value. (\*) means that some value has to be provided, whereas for (?) the entry value is optional. > For example, if an application provides a view to editing some personal data, the qualifier could look as follows: `{entity: 'person', id: '*'}`.\ -Other applications can invoke this capability by issuing an intent matching the capability's qualifier, with any value allowed for the 'id'. +Applications can invoke this capability by issuing an intent matching the capability's qualifier, with any value allowed for the 'id'.\ + +> For example, if the application provides a single view to edit or create a person, the qualifier could look as follows: `{entity: 'person', id: '?'}`.\ +Applications can invoke this capability by issuing an intent without an id for creation (`{entity: 'person'}`), or with an id for editing (`{entity: 'person', id: 1}`). Some capabilities define an entry point URL if showing an application page. Using placeholders in URL segments is possible. When invoked, they are replaced with values from the intent qualifier. @@ -72,7 +75,7 @@ There are some built-in capability types supported by the platform. However, the |manifest-registry|allows querying the manifest registry| #### Intent -If an application intends to interact with functionality of another application, it must declare a respective intent in its manifest. An application has implicit intents for all its own capabilities. Just like for capabilities, it is allowed to use the wildcard character (*) as qualifier value or as qualifier key. +If an application intends to interact with functionality of another application, it must declare a respective intent in its manifest. An application has implicit intents for all its own capabilities. It is allowed to use the wildcard character (*) as qualifier key and/or wildcard characters (\*) and (?) as qualifier value. ### Limitation and restrictions The platform starts a separate application instance for every entry point invoked. The only exception is when navigating within the same view, but only if the application uses hash-based URLs. The fact of having multiple instances brings some requirements and limitations which you should be aware of: diff --git a/resources/site/how-to/workbench-application-platform/how-to-install-a-programmatic-intent-handler.md b/resources/site/how-to/workbench-application-platform/how-to-install-a-programmatic-intent-handler.md index ec0edf6bc..47a8ed473 100644 --- a/resources/site/how-to/workbench-application-platform/how-to-install-a-programmatic-intent-handler.md +++ b/resources/site/how-to/workbench-application-platform/how-to-install-a-programmatic-intent-handler.md @@ -7,8 +7,6 @@ The platform allows handling intents in the host application. It is like providing a capability in a sub-application, but in the host application. -The handler can be configured to act as a proxy which the platform invokes only if some application provides a respective capability. For example, `ViewIntentHandler` is a proxy for view capabilities. Upon a view intent, the handler loads capability metadata from the application providing that view, reads the view path and opens the view via the workbench. - Open `app.module.ts` and register the intent handler as multi provider under DI injection `INTENT_HANDLER` ```typescript @@ -32,16 +30,15 @@ export class CustomIntentHandler implements IntentHandler { public readonly type = '...'; ➀ public readonly qualifier: Qualifier = {...}; ➁ - public readonly proxy = false; ➂ - public readonly description = '...'; ➃ + public readonly description = '...'; ➂ public onInit(applicationRegistry: ApplicationRegistry, manifestRegistry: ManifestRegistry): void { - ... ➄ + ... ➃ } public onIntent(envelope: MessageEnvelope<IntentMessage>): void { - ... ➅ + ... ➄ } } ``` @@ -49,10 +46,9 @@ export class CustomIntentHandler implements IntentHandler { |-|-| |➀|Sets the type of functionality which this handler provides, e.g. 'auth-token' to reply with the auth-token.| |➁|Sets the qualifier which intents must have to be handled. If not set, `NilQualifier` is used.| -|➂|Indicates if this handler acts as a proxy through which intents are processed, which is `false` by default.| -|➃|Describes the capability this handler provides.| -|➄|Optional lifecycle hook that is called after the platform completed registration of applications.<br>Use this method to handle any initialization tasks which require the application or manifest registry.| -|➅|Method invoked upon the receipt of an intent.| +|➂|Describes the capability this handler provides.| +|➃|Optional lifecycle hook that is called after the platform completed registration of applications.<br>Use this method to handle any initialization tasks which require the application or manifest registry.| +|➄|Method invoked upon the receipt of an intent.| ## How to issue intents to the programmatic intent handler