From 4ae3ca5bf2cd0c10f0dfca94e99f66972fafc352 Mon Sep 17 00:00:00 2001 From: Tobias Ortmayr Date: Mon, 22 Mar 2021 08:33:29 +0100 Subject: [PATCH] Provide API to filter unwanted contributions Adds the ContributionFilterRegistry which can be used to filter contributions of Theia extensions before they are bound. This mechanism can be used by application developers to specifically filter individual contributions from Theia extensions they have no control over, i.e. core or 3rd party extensions. Resolves #9069 Contributed on behalf of STMicroelectronics Signed-off-by: Tobias Ortmayr --- CHANGELOG.md | 6 + .../browser/api-samples-frontend-module.ts | 2 + .../sample-filtered-command-contribution.ts | 50 +++++++++ .../api-tests/src/contribution-filter.spec.js | 36 ++++++ .../browser/frontend-application-module.ts | 4 + .../contribution-filter-registry.ts | 69 ++++++++++++ .../contribution-filter.ts | 40 +++++++ .../src/common/contribution-filter/filter.ts | 48 ++++++++ .../src/common/contribution-filter/index.ts | 20 ++++ .../string-based-filter.ts | 106 ++++++++++++++++++ .../core/src/common/contribution-provider.ts | 8 ++ packages/core/src/common/index.ts | 1 + .../src/node/backend-application-module.ts | 3 + 13 files changed, 393 insertions(+) create mode 100644 examples/api-samples/src/browser/contribution-filter/sample-filtered-command-contribution.ts create mode 100644 examples/api-tests/src/contribution-filter.spec.js create mode 100644 packages/core/src/common/contribution-filter/contribution-filter-registry.ts create mode 100644 packages/core/src/common/contribution-filter/contribution-filter.ts create mode 100644 packages/core/src/common/contribution-filter/filter.ts create mode 100644 packages/core/src/common/contribution-filter/index.ts create mode 100644 packages/core/src/common/contribution-filter/string-based-filter.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f262753da048b..af147fb283c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## v1.13.0 + +- [core] add API to filter contributions at runtime [#9317](https://github.com/eclipse-theia/theia/pull/9317) Contributed on behalf of STMicroelectronics + +[Breaking Changes:](#breaking_changes_1.13.0) + ## v1.12.0 - 3/25/2020 [1.12.0 Milestone](https://github.com/eclipse-theia/theia/milestone/17) diff --git a/examples/api-samples/src/browser/api-samples-frontend-module.ts b/examples/api-samples/src/browser/api-samples-frontend-module.ts index d06b8fb0ea21e..10a3ba15d7ecc 100644 --- a/examples/api-samples/src/browser/api-samples-frontend-module.ts +++ b/examples/api-samples/src/browser/api-samples-frontend-module.ts @@ -16,6 +16,7 @@ import { ContainerModule } from '@theia/core/shared/inversify'; import { bindDynamicLabelProvider } from './label/sample-dynamic-label-provider-command-contribution'; +import { bindSampleFilteredCommandContribution } from './contribution-filter/sample-filtered-command-contribution'; import { bindSampleUnclosableView } from './view/sample-unclosable-view-contribution'; import { bindSampleOutputChannelWithSeverity } from './output/sample-output-channel-with-severity'; import { bindSampleMenu } from './menu/sample-menu-contribution'; @@ -31,4 +32,5 @@ export default new ContainerModule(bind => { bindSampleMenu(bind); bindSampleFileWatching(bind); bindVSXCommand(bind); + bindSampleFilteredCommandContribution(bind); }); diff --git a/examples/api-samples/src/browser/contribution-filter/sample-filtered-command-contribution.ts b/examples/api-samples/src/browser/contribution-filter/sample-filtered-command-contribution.ts new file mode 100644 index 0000000000000..5ea30b68ec5c9 --- /dev/null +++ b/examples/api-samples/src/browser/contribution-filter/sample-filtered-command-contribution.ts @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { Command, CommandContribution, CommandRegistry, ContributionFilter, equals, NameBasedContributionFilter } from '@theia/core/lib/common'; +import { injectable, interfaces } from '@theia/core/shared/inversify'; +export namespace SampleFilteredCommand { + const EXAMPLE_CATEGORY = 'Examples'; + export const FILTERED: Command = { + id: 'example_command.filtered', + category: EXAMPLE_CATEGORY, + label: 'This command should be filtered out' + }; +} + +/** + * This sample command is used to test the runtime filtering of already bound contributions. + */ +@injectable() +export class SampleFilteredCommandContribution implements CommandContribution { + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(SampleFilteredCommand.FILTERED, { execute: () => { } }); + } +} + +@injectable() +export class SampleFilteredCommandContributionFilter extends NameBasedContributionFilter { + + contributions = [CommandContribution]; + doTest(toTest: string): boolean { + return equals(toTest, false, 'SampleFilteredCommandContribution'); + } +} + +export const bindSampleFilteredCommandContribution = (bind: interfaces.Bind) => { + bind(CommandContribution).to(SampleFilteredCommandContribution); + bind(ContributionFilter).to(SampleFilteredCommandContributionFilter); +}; diff --git a/examples/api-tests/src/contribution-filter.spec.js b/examples/api-tests/src/contribution-filter.spec.js new file mode 100644 index 0000000000000..c5267ed4496e4 --- /dev/null +++ b/examples/api-tests/src/contribution-filter.spec.js @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// @ts-check +describe('Contribution filter', function () { + this.timeout(5000); + const { assert } = chai; + + const { CommandRegistry, CommandContribution } = require('@theia/core/lib/common/command'); + const { SampleFilteredCommandContribution, SampleFilteredCommand } = require('@theia/api-samples/lib/browser/contribution-filter/sample-filtered-command-contribution'); + + const container = window.theia.container; + const commands = container.get(CommandRegistry); + + it('filtered command in container but not in registry', async function () { + const allCommands = container.getAll(CommandContribution); + assert.isDefined(allCommands.find(contribution => contribution instanceof SampleFilteredCommandContribution), + 'SampleFilteredCommandContribution is not bound in container'); + const filteredCommand = commands.getCommand(SampleFilteredCommand.FILTERED.id); + assert.isUndefined(filteredCommand, 'SampleFilteredCommandContribution should be filtered out but is present in "CommandRegistry"'); + }); + +}); diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 5aad42ea55905..602bd63a59911 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -97,6 +97,7 @@ import { LanguageService } from './language-service'; import { EncodingRegistry } from './encoding-registry'; import { EncodingService } from '../common/encoding-service'; import { AuthenticationService, AuthenticationServiceImpl } from '../browser/authentication-service'; +import { ContributionFilterRegistry } from '../common/contribution-filter'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -342,4 +343,7 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(ContextMenuContext).toSelf().inSingletonScope(); bind(AuthenticationService).to(AuthenticationServiceImpl).inSingletonScope(); + + bind(ContributionFilterRegistry).toSelf().inSingletonScope(); + }); diff --git a/packages/core/src/common/contribution-filter/contribution-filter-registry.ts b/packages/core/src/common/contribution-filter/contribution-filter-registry.ts new file mode 100644 index 0000000000000..417108edf837b --- /dev/null +++ b/packages/core/src/common/contribution-filter/contribution-filter-registry.ts @@ -0,0 +1,69 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, multiInject, optional } from 'inversify'; +import { ContributionFilter, ContributionType } from './contribution-filter'; +import { applyFilters } from './filter'; + +const GENERIC_CONTRIBUTION_FILTER_KEY = '*'; +@injectable() +export class ContributionFilterRegistry { + registry: Map; + constructor(@multiInject(ContributionFilter) @optional() contributionFilters: ContributionFilter[] = []) { + this.registry = new Map(); + contributionFilters.forEach(filter => { + if (!filter.contributions || filter.contributions.length === 0) { + this.addFilter(GENERIC_CONTRIBUTION_FILTER_KEY, filter); + } else { + filter.contributions.forEach(type => { + this.addFilter(type, filter); + }); + } + }); + } + + private addFilter(type: ContributionType, filter: ContributionFilter): void { + this.getOrCreate(type).push(filter); + } + + private getOrCreate(type: ContributionType): ContributionFilter[] { + let value = this.registry.get(type); + if (!value) { + value = []; + this.registry.set(type, value); + } + return value; + } + + get(type: ContributionType): ContributionFilter[] { + const filters = [...(this.registry.get(type) || [])]; + if (type !== GENERIC_CONTRIBUTION_FILTER_KEY) { + filters.push(...(this.registry.get(GENERIC_CONTRIBUTION_FILTER_KEY) || [])); + } + return filters; + } + + /** + * Applies the filters for the given contribution type. Generic filters will be applied on any given type. + * @param toFilter the elements to filter + * @param type the contribution type for which potentially filters were registered + * @returns the filtered elements + */ + applyFilters(toFilter: T[], type: ContributionType): T[] { + const filters = this.get(type); + return applyFilters(toFilter, filters); + } +} diff --git a/packages/core/src/common/contribution-filter/contribution-filter.ts b/packages/core/src/common/contribution-filter/contribution-filter.ts new file mode 100644 index 0000000000000..b423e015a6bbc --- /dev/null +++ b/packages/core/src/common/contribution-filter/contribution-filter.ts @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { interfaces } from 'inversify'; +import { Filter } from './filter'; +import { NameBasedFilter } from './string-based-filter'; +export type ContributionType = interfaces.ServiceIdentifier; + +export const ContributionFilter = Symbol('ContributionFilter'); + +/** + * Specialized `Filter` that is used by the `ContainerBasedContributionProvider` to + * filter unwanted contributions that are already bound in the DI container. + */ +export interface ContributionFilter extends Filter { + /** + * Contribution types for which this filter is applicable. If `undefined` or empty this filter + * will be applied to all contribution types. + */ + contributions?: ContributionType[]; +} + +/** + * Specialized `ContributionFilter` that can be used to filter contributions based on their constructor name. + */ +export abstract class NameBasedContributionFilter extends NameBasedFilter implements ContributionFilter { +} diff --git a/packages/core/src/common/contribution-filter/filter.ts b/packages/core/src/common/contribution-filter/filter.ts new file mode 100644 index 0000000000000..66eba387fb55c --- /dev/null +++ b/packages/core/src/common/contribution-filter/filter.ts @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +export const Filter = Symbol('Filter'); +/** + * A `Filter` can be used to test whether a given object should be filtered + * from a set of objects. The `test` function can be applied to an object + * of matching type and returns `true` if the object should be filtered out. + */ +export interface Filter { + /** + * Evaluates this filter on the given argument. + * @param toTest Object that should be tested + * @returns `true` if the object should be filtered out, `false` otherwise + */ + test(toTest: T): Boolean; +} + +/** + * Applies a set of filters to a set of given objects and returns the set of filtered objects. + * @param toFilter Set of objects which should be filtered + * @param filters Set of filters that should be applied + * @param negate Negation flag. If set to true the result of all `Filter.test` methods is negated + * @returns The set of filtered arguments + */ +export function applyFilters(toFilter: T[], filters: Filter[], negate: boolean = false): T[] { + if (filters.length === 0) { + return toFilter; + } + return toFilter.filter(object => { + const result = filters.every(filter => !filter.test(object)); + return negate ? !result : result; + }); +} + diff --git a/packages/core/src/common/contribution-filter/index.ts b/packages/core/src/common/contribution-filter/index.ts new file mode 100644 index 0000000000000..3879774eb5ee2 --- /dev/null +++ b/packages/core/src/common/contribution-filter/index.ts @@ -0,0 +1,20 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +export * from './contribution-filter'; +export * from './contribution-filter-registry'; +export * from './filter'; +export * from './string-based-filter'; diff --git a/packages/core/src/common/contribution-filter/string-based-filter.ts b/packages/core/src/common/contribution-filter/string-based-filter.ts new file mode 100644 index 0000000000000..c1b47ed593576 --- /dev/null +++ b/packages/core/src/common/contribution-filter/string-based-filter.ts @@ -0,0 +1,106 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import { Filter } from './filter'; + +/** + * Specialized `Filter` class for filters based on string level. Test objects + * are converted to a corresponding string representation with the help of the `toString()`-method + * which enable to implement a `doTest()`-method based on string comparison. + */ +@injectable() +export abstract class StringBasedFilter implements Filter { + + /** + * Converts the test object to a corresponding string representation + * that is then used in the `doTest()` method. + * @param toTest Test object that should be converted to string + */ + abstract toString(toTest: T): string | undefined; + + /** + * Tests whether the object should be filtered out based on its string representation. + * @param testStr String representation of the test object + * @returns `true` if the object should be filtered out, `false` otherwise + */ + abstract doTest(testStr: string): boolean; + + test(contribution: T): boolean { + const testStr = this.toString(contribution); + if (typeof testStr !== 'string') { + return false; + } + + return this.doTest(testStr); + } +} +/** + * Utility function to test wether any of the given string patterns is included in the + * tested string. + * @param testStr String that should be tested + * @param ignoreCase Flag, to indicate wether the test should be case-sensitive or not + * @param patterns The set of patterns that should be tested for inclusion + * @returns `true` if any of the given patterns is included in the test string, `false` otherwise + */ +export function includes(testStr: string, ignoreCase: boolean, ...patterns: string[]): boolean { + if (ignoreCase) { + testStr = testStr.toLowerCase(); + patterns = patterns.map(pattern => pattern.toLowerCase()); + } + return patterns.find(pattern => testStr.includes(pattern)) !== undefined; +} + +/** + * Utility function to test wether any of the given string patterns is equal to the + * tested string. + * @param testStr String that should be tested + * @param ignoreCase Flag, to indicate wether the test should be case-sensitive or not + * @param patterns The set of patterns that should be tested for equality + * @returns `true` if any of the given patterns is equal to the test string, `false` otherwise + */ +export function equals(testStr: string, ignoreCase: boolean, ...patterns: string[]): boolean { + if (ignoreCase) { + testStr = testStr.toLowerCase(); + patterns = patterns.map(pattern => pattern.toLowerCase()); + } + return patterns.find(pattern => testStr === pattern) !== undefined; + +} + +/** + * Utility function to test wether a string matches any of the given regular expression patterns. + * @param testStr String that should be tested + * @param ignoreCase Flag, to indicate wether the test should be case-sensitive or not + * @param patterns The set of regular expressions that should be matched + * @returns `true` if the test string matches any of the given regular expressions, `false` otherwise + */ +export function matches(testStr: string, ignoreCase: boolean, ...patterns: (RegExp | string)[]): boolean { + const flags = ignoreCase ? 'i' : undefined; + const regexps = patterns.map(pattern => new RegExp(pattern, flags)); + return regexps.find(regexp => regexp.test(testStr)) !== undefined; +} + +/** + * Specialized `StringBasedFilter` that can be used to filter objects based on their constructor name. + */ +@injectable() +export abstract class NameBasedFilter extends StringBasedFilter { + toString(toTest: T): string { + return toTest.constructor.name; + } +} + diff --git a/packages/core/src/common/contribution-provider.ts b/packages/core/src/common/contribution-provider.ts index 3a4605e3e4376..3ab0cd703b4ba 100644 --- a/packages/core/src/common/contribution-provider.ts +++ b/packages/core/src/common/contribution-provider.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import { interfaces } from 'inversify'; +import { ContributionFilterRegistry } from './contribution-filter'; export const ContributionProvider = Symbol('ContributionProvider'); @@ -29,6 +30,7 @@ export interface ContributionProvider { class ContainerBasedContributionProvider implements ContributionProvider { protected services: T[] | undefined; + protected filterRegistry: ContributionFilterRegistry | undefined; constructor( protected readonly serviceIdentifier: interfaces.ServiceIdentifier, @@ -48,11 +50,17 @@ class ContainerBasedContributionProvider implements Contributi console.error(error); } } + if (!this.filterRegistry && currentContainer.isBound(ContributionFilterRegistry)) { + this.filterRegistry = currentContainer.get(ContributionFilterRegistry); + } // eslint-disable-next-line no-null/no-null currentContainer = recursive === true ? currentContainer.parent : null; } this.services = currentServices; } + if (this.filterRegistry) { + return this.filterRegistry.applyFilters(this.services, this.serviceIdentifier); + } return this.services; } } diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 25f12ec4385a4..7d6fecbd4a480 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -37,6 +37,7 @@ export * from './selection'; export * from './strings'; export * from './application-error'; export * from './lsp-types'; +export * from './contribution-filter'; import { environment } from '@theia/application-package/lib/environment'; export { environment }; diff --git a/packages/core/src/node/backend-application-module.ts b/packages/core/src/node/backend-application-module.ts index 41d43532205d6..8627ea17d80ef 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -30,6 +30,7 @@ import { EnvVariablesServerImpl } from './env-variables'; import { ConnectionContainerModule } from './messaging/connection-container-module'; import { QuickPickService, quickPickServicePath } from '../common/quick-pick-service'; import { WsRequestValidator, WsRequestValidatorContribution } from './ws-request-validators'; +import { ContributionFilterRegistry } from '../common/contribution-filter'; decorate(injectable(), ApplicationPackage); @@ -85,4 +86,6 @@ export const backendApplicationModule = new ContainerModule(bind => { bind(WsRequestValidator).toSelf().inSingletonScope(); bindContributionProvider(bind, WsRequestValidatorContribution); + + bind(ContributionFilterRegistry).toSelf().inSingletonScope(); });