diff --git a/gradle/changelog/order_repo_info_ext.yaml b/gradle/changelog/order_repo_info_ext.yaml new file mode 100644 index 0000000000..33fab1dd38 --- /dev/null +++ b/gradle/changelog/order_repo_info_ext.yaml @@ -0,0 +1,4 @@ +- type: changed + description: Set order priority for repository information extensions ([#2041](https://github.com/scm-manager/scm-manager/pull/2041)) +- type: fixed + description: Resource bundle loading from plugins ([#2041](https://github.com/scm-manager/scm-manager/pull/2041)) diff --git a/scm-core/src/main/java/sonia/scm/cli/CliResourceBundle.java b/scm-core/src/main/java/sonia/scm/cli/CliResourceBundle.java new file mode 100644 index 0000000000..f7911bf10c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/cli/CliResourceBundle.java @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.cli; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines a custom resource bundle for the CLI command + * + * We need to use this workaround instead the picocli way because we cannot ensure that the resource bundles can be found by the classloader. + * Currently there is no solution for picocli to chose which classloader should be used to load command-related resource bundle. + * + * @since 2.35.0 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface CliResourceBundle { + String value(); +} diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.test.ts b/scm-plugins/scm-git-plugin/src/main/js/index.test.ts index ed35c83cfe..d85f45d54c 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/index.test.ts +++ b/scm-plugins/scm-git-plugin/src/main/js/index.test.ts @@ -22,23 +22,17 @@ * SOFTWARE. */ -import "@scm-manager/ui-tests/i18n"; +import "@scm-manager/ui-tests"; +import { Repository } from "@scm-manager/ui-types"; import { gitPredicate } from "./index"; +const repository: Repository = { _links: {}, namespace: "hitchhiker", name: "HeartOfGold", type: "git" }; + describe("test git predicate", () => { it("should return false", () => { - expect(gitPredicate(undefined)).toBe(false); - expect(gitPredicate({})).toBe(false); expect( gitPredicate({ - repository: {}, - }) - ).toBe(false); - expect( - gitPredicate({ - repository: { - type: "hg", - }, + repository: { ...repository, type: "hg" }, }) ).toBe(false); }); @@ -46,9 +40,7 @@ describe("test git predicate", () => { it("should return true", () => { expect( gitPredicate({ - repository: { - type: "git", - }, + repository, }) ).toBe(true); }); diff --git a/scm-plugins/scm-git-plugin/src/main/js/index.ts b/scm-plugins/scm-git-plugin/src/main/js/index.ts index c32e969aa9..e7ec27878c 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/index.ts +++ b/scm-plugins/scm-git-plugin/src/main/js/index.ts @@ -36,15 +36,14 @@ import GitTagInformation from "./GitTagInformation"; // repository // @visibleForTesting -export const gitPredicate = (props: any) => { +export const gitPredicate = (props: extensionPoints.RepositoryDetailsInformation["props"]) => { return !!(props && props.repository && props.repository.type === "git"); }; -binder.bind( - "repos.repository-details.information", - ProtocolInformation, - gitPredicate -); +binder.bind("repos.repository-details.information", ProtocolInformation, { + predicate: gitPredicate, + priority: 100, +}); binder.bind("repos.branch-details.information", GitBranchInformation, { priority: 100, predicate: gitPredicate }); binder.bind( diff --git a/scm-plugins/scm-hg-plugin/src/main/js/index.ts b/scm-plugins/scm-hg-plugin/src/main/js/index.ts index 9083e6e4f3..2a3b56c1f4 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/index.ts +++ b/scm-plugins/scm-hg-plugin/src/main/js/index.ts @@ -31,15 +31,14 @@ import HgBranchInformation from "./HgBranchInformation"; import HgTagInformation from "./HgTagInformation"; import HgRepositoryConfigurationForm from "./HgRepositoryConfigurationForm"; -const hgPredicate = (props: any) => { +const hgPredicate = (props: extensionPoints.RepositoryDetailsInformation["props"]) => { return props.repository && props.repository.type === "hg"; }; -binder.bind( - "repos.repository-details.information", - ProtocolInformation, - hgPredicate -); +binder.bind("repos.repository-details.information", ProtocolInformation, { + predicate: hgPredicate, + priority: 100, +}); binder.bind("repos.branch-details.information", HgBranchInformation, { priority: 100, predicate: hgPredicate }); binder.bind( diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CliProcessor.java b/scm-webapp/src/main/java/sonia/scm/cli/CliProcessor.java index faa5779ad7..e9009dd362 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/CliProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/CliProcessor.java @@ -27,6 +27,7 @@ package sonia.scm.cli; import com.google.inject.Injector; import picocli.AutoComplete; import picocli.CommandLine; +import sonia.scm.plugin.PluginLoader; import javax.inject.Inject; import java.util.Locale; @@ -34,16 +35,19 @@ import java.util.ResourceBundle; public class CliProcessor { + private static final String CORE_RESOURCE_BUNDLE = "sonia.scm.cli.i18n"; private final CommandRegistry registry; private final Injector injector; private final CommandLine.Model.CommandSpec usageHelp; private final CliExceptionHandlerFactory exceptionHandlerFactory; + private final PluginLoader pluginLoader; @Inject - public CliProcessor(CommandRegistry registry, Injector injector, CliExceptionHandlerFactory exceptionHandlerFactory) { + public CliProcessor(CommandRegistry registry, Injector injector, CliExceptionHandlerFactory exceptionHandlerFactory, PluginLoader pluginLoader) { this.registry = registry; this.injector = injector; this.exceptionHandlerFactory = exceptionHandlerFactory; + this.pluginLoader = pluginLoader; this.usageHelp = new CommandLine(HelpMixin.class).getCommandSpec(); } @@ -51,7 +55,7 @@ public class CliProcessor { CommandFactory factory = new CommandFactory(injector, context); CommandLine cli = new CommandLine(ScmManagerCommand.class, factory); cli.getCommandSpec().addMixin("help", usageHelp); - cli.setResourceBundle(getBundle("sonia.scm.cli.i18n", context.getLocale())); + cli.setResourceBundle(getBundle(CORE_RESOURCE_BUNDLE, context.getLocale())); for (RegisteredCommandNode c : registry.createCommandTree()) { CommandLine commandline = createCommandline(context, factory, c); cli.getCommandSpec().addSubcommand(c.getName(), commandline); @@ -68,10 +72,13 @@ public class CliProcessor { CommandLine commandLine = new CommandLine(command.getCommand(), factory); commandLine.getCommandSpec().addMixin("help", usageHelp); - ResourceBundle resourceBundle = commandLine.getCommandSpec().resourceBundle(); - if (resourceBundle != null) { - String resourceBundleBaseName = resourceBundle.getBaseBundleName(); - commandLine.setResourceBundle(getBundle(resourceBundleBaseName, context.getLocale())); + CliResourceBundle customResourceBundle = command.getCommand().getAnnotation(CliResourceBundle.class); + if (customResourceBundle != null) { + String resourceBundleBaseName = customResourceBundle.value(); + ResourceBundle pluginResourceBundle = getBundle(resourceBundleBaseName, context.getLocale()); + ResourceBundle coreResourceBundle = getBundle(CORE_RESOURCE_BUNDLE, context.getLocale()); + CombinedResourceBundle combinedResourceBundle = new CombinedResourceBundle(pluginResourceBundle, coreResourceBundle); + commandLine.setResourceBundle(combinedResourceBundle); } for (RegisteredCommandNode child : command.getChildren()) { if (!commandLine.getCommandSpec().subcommands().containsKey(child.getName())) { @@ -84,7 +91,7 @@ public class CliProcessor { } private ResourceBundle getBundle(String baseName, Locale locale) { - return ResourceBundle.getBundle(baseName, locale, new ResourceBundle.Control() { + return ResourceBundle.getBundle(baseName, locale, pluginLoader.getUberClassLoader(), new ResourceBundle.Control() { @Override public Locale getFallbackLocale(String baseName, Locale locale) { return Locale.ROOT; diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CombinedResourceBundle.java b/scm-webapp/src/main/java/sonia/scm/cli/CombinedResourceBundle.java new file mode 100644 index 0000000000..725519ecc7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/CombinedResourceBundle.java @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.cli; + +import java.util.Arrays; +import java.util.Collections; +import java.util.ListResourceBundle; +import java.util.ResourceBundle; +import java.util.stream.Collectors; + +public class CombinedResourceBundle extends ListResourceBundle { + + private final Object[][] contents; + + @Override + protected Object[][] getContents() { + return contents; + } + + public CombinedResourceBundle(ResourceBundle... bundles) { + this.contents = Arrays + .stream(bundles) + .flatMap(resourceBundle -> + Collections + .list(resourceBundle.getKeys()) + .stream().map(key -> new Object[]{key, resourceBundle.getObject(key)})) + .collect(Collectors.toList()) + .toArray(new Object[0][0]); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java b/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java index 7892a22bf9..0694efbd5c 100644 --- a/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java @@ -35,6 +35,7 @@ import org.mockito.Answers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import picocli.CommandLine; +import sonia.scm.plugin.PluginLoader; import javax.annotation.Nonnull; import java.io.ByteArrayOutputStream; @@ -55,6 +56,8 @@ class CliProcessorTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private CliContext context; + @Mock + private PluginLoader pluginLoader; @Mock private CliExceptionHandlerFactory exceptionHandlerFactory; @Mock @@ -62,6 +65,11 @@ class CliProcessorTest { @Mock private CliParameterExceptionHandler parameterExceptionHandler; + @BeforeEach + void mockPluginLoader() { + when(pluginLoader.getUberClassLoader()).thenReturn(this.getClass().getClassLoader()); + } + @Nested class ForDefaultLanguageTest { @@ -76,7 +84,7 @@ class CliProcessorTest { void shouldExecutePingCommand() { when(registry.createCommandTree()).thenReturn(ImmutableList.of(new RegisteredCommandNode("ping", PingCommand.class))); Injector injector = Guice.createInjector(); - CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory); + CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory, pluginLoader); cliProcessor.execute(context, "ping"); @@ -87,7 +95,7 @@ class CliProcessorTest { void shouldExecutePingCommandWithExitCode0() { when(registry.createCommandTree()).thenReturn(ImmutableList.of(new RegisteredCommandNode("ping", PingCommand.class))); Injector injector = Guice.createInjector(); - CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory); + CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory, pluginLoader); int exitCode = cliProcessor.execute(context, "ping"); @@ -170,7 +178,7 @@ class CliProcessorTest { when(context.getStdout()).thenReturn(new PrintWriter(baos)); Injector injector = Guice.createInjector(); - CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory); + CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory, pluginLoader); cliProcessor.execute(context, args); return baos.toString(); @@ -194,7 +202,8 @@ class CliProcessorTest { } } - @CommandLine.Command(name = "three", resourceBundle = "sonia.scm.cli.test") + @CommandLine.Command(name = "three") + @CliResourceBundle("sonia.scm.cli.test") static class SubSubCommand implements Runnable { @Override public void run() {