From 162dd6ad0a1e86c20364d8aba23cf7ecd5024778 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 4 Apr 2022 12:02:16 +0200 Subject: [PATCH] CLI Support for repository actions (#1987) To make SCM-Manager more accessible and to make it easier using scripts against the server, we created a command line interface. This command line interface can be used to perform the default actions like create, modify and delete repositories. It is also very flexible and can be extended by plugins. The CLI already supports internationalization, help texts, input validation, loose and table-like templates and nested subcommands. Check the cli guidelines to learn how add new cli commands. Co-authored-by: Sebastian Sdorra --- docs/en/development/cli-guideline.md | 139 ++++++++++++++ gradle/changelog/cli.yaml | 2 + gradle/dependencies.gradle | 6 + scm-annotation-processor/build.gradle | 3 + scm-annotation-processor/gradle.lockfile | 1 + .../annotation/ScmAnnotationProcessor.java | 7 +- .../java/sonia/scm/cli/ParentCommand.java | 36 ++++ scm-core/build.gradle | 4 + scm-core/gradle.lockfile | 23 ++- .../main/java/sonia/scm/cli/CliContext.java | 68 +++++++ .../main/java/sonia/scm/cli/CliException.java | 41 ++++ .../java/sonia/scm/cli/CommandValidator.java | 117 ++++++++++++ .../src/main/java/sonia/scm/cli/ExitCode.java | 39 ++++ .../src/main/java/sonia/scm/cli/Table.java | 175 ++++++++++++++++++ .../java/sonia/scm/cli/TemplateRenderer.java | 166 +++++++++++++++++ .../scm/cli/TemplateRenderingException.java | 37 ++++ .../sonia/scm/plugin/NamedClassElement.java | 49 +++++ .../main/java/sonia/scm/plugin/ScmModule.java | 7 + .../sonia/scm/repository/RepositoryName.java | 75 ++++++++ .../RepositoryNameConstrainValidator.java | 60 ++++++ .../repository/RepositoryTypeConstraint.java | 48 +++++ .../RepositoryTypeConstraintValidator.java | 78 ++++++++ .../scm/repository/cli/RepositoryCommand.java | 30 +++ .../resources/ValidationMessages.properties | 26 +++ .../ValidationMessages_de.properties | 26 +++ .../sonia/scm/cli/CommandValidatorTest.java | 126 +++++++++++++ .../sonia/scm/cli/TemplateRendererTest.java | 109 +++++++++++ .../RepositoryNameConstrainValidatorTest.java | 162 ++++++++++++++++ ...RepositoryTypeConstraintValidatorTest.java | 152 +++++++++++++++ .../resources/sonia/scm/cli/i18n.properties | 25 +++ .../sonia/scm/cli/i18n_de.properties | 25 +++ scm-dao-xml/gradle.lockfile | 1 + scm-it/gradle.lockfile | 1 + scm-plugins/scm-git-plugin/gradle.lockfile | 3 +- scm-plugins/scm-hg-plugin/gradle.lockfile | 3 +- .../gradle.lockfile | 3 +- scm-plugins/scm-legacy-plugin/gradle.lockfile | 3 +- scm-plugins/scm-svn-plugin/gradle.lockfile | 3 +- scm-test/gradle.lockfile | 1 + scm-webapp/gradle.lockfile | 1 + .../scm/api/v2/resources/CliResource.java | 95 ++++++++++ .../sonia/scm/cli/CliExceptionHandler.java | 52 ++++++ .../java/sonia/scm/cli/CliExitException.java | 37 ++++ .../main/java/sonia/scm/cli/CliProcessor.java | 81 ++++++++ .../java/sonia/scm/cli/CommandFactory.java | 57 ++++++ .../java/sonia/scm/cli/CommandRegistry.java | 69 +++++++ .../main/java/sonia/scm/cli/HelpMixin.java | 33 ++++ .../scm/cli/JsonStreamingCliContext.java | 158 ++++++++++++++++ .../java/sonia/scm/cli/LogoutCommand.java | 30 +++ .../NonExistingParentCommandException.java | 35 ++++ .../main/java/sonia/scm/cli/PingCommand.java | 45 +++++ .../java/sonia/scm/cli/RegisteredCommand.java | 36 ++++ .../scm/cli/RegisteredCommandCollector.java | 84 +++++++++ .../sonia/scm/cli/RegisteredCommandNode.java | 42 +++++ .../java/sonia/scm/cli/ScmManagerCommand.java | 30 +++ .../modules/ApplicationModuleProvider.java | 2 + .../repository/cli/RepositoryCommandDto.java | 43 +++++ .../cli/RepositoryCreateCommand.java | 116 ++++++++++++ .../cli/RepositoryDeleteCommand.java | 85 +++++++++ .../repository/cli/RepositoryGetCommand.java | 72 +++++++ .../repository/cli/RepositoryListCommand.java | 89 +++++++++ .../cli/RepositoryModifyCommand.java | 95 ++++++++++ .../cli/RepositoryTemplateRenderer.java | 86 +++++++++ ...epositoryToRepositoryCommandDtoMapper.java | 69 +++++++ .../java/sonia/scm/security/ApiKeyRealm.java | 18 +- .../validation/DefaultValidatorProvider.java | 49 +++++ .../GuiceConstraintValidatorFactory.java | 51 +++++ .../scm/validation/ResteasyValidator.java | 78 ++++++++ .../ResteasyValidatorContextResolver.java | 59 ++++++ .../scm/validation/ValidationModule.java | 39 ++++ .../resources/sonia/scm/cli/i18n.properties | 81 ++++++++ .../sonia/scm/cli/i18n_de.properties | 81 ++++++++ .../java/sonia/scm/cli/CliProcessorTest.java | 171 +++++++++++++++++ .../sonia/scm/cli/CommandRegistryTest.java | 96 ++++++++++ .../scm/cli/JsonStreamingCliContextTest.java | 89 +++++++++ .../cli/RegisteredCommandCollectorTest.java | 93 ++++++++++ .../cli/RepositoryCreateCommandTest.java | 108 +++++++++++ .../cli/RepositoryDeleteCommandTest.java | 81 ++++++++ .../cli/RepositoryGetCommandTest.java | 88 +++++++++ .../cli/RepositoryListCommandTest.java | 89 +++++++++ .../cli/RepositoryModifyCommandTest.java | 90 +++++++++ ...itoryToRepositoryCommandDtoMapperTest.java | 95 ++++++++++ .../DefaultValidatorProviderTest.java | 78 ++++++++ .../GuiceConstraintValidatorFactoryTest.java | 51 +++++ .../validation/RepositoryManagerModule.java | 42 +++++ .../sonia/scm/validation/RepositoryType.java | 43 +++++ .../RepositoryTypeConstraintValidator.java | 51 +++++ .../scm/validation/ValidationModuleTest.java | 132 +++++++++++++ .../resources/sonia/scm/cli/test.properties | 24 +++ .../sonia/scm/cli/test_de.properties | 25 +++ 90 files changed, 5303 insertions(+), 21 deletions(-) create mode 100644 docs/en/development/cli-guideline.md create mode 100644 gradle/changelog/cli.yaml create mode 100644 scm-annotations/src/main/java/sonia/scm/cli/ParentCommand.java create mode 100644 scm-core/src/main/java/sonia/scm/cli/CliContext.java create mode 100644 scm-core/src/main/java/sonia/scm/cli/CliException.java create mode 100644 scm-core/src/main/java/sonia/scm/cli/CommandValidator.java create mode 100644 scm-core/src/main/java/sonia/scm/cli/ExitCode.java create mode 100644 scm-core/src/main/java/sonia/scm/cli/Table.java create mode 100644 scm-core/src/main/java/sonia/scm/cli/TemplateRenderer.java create mode 100644 scm-core/src/main/java/sonia/scm/cli/TemplateRenderingException.java create mode 100644 scm-core/src/main/java/sonia/scm/plugin/NamedClassElement.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/RepositoryName.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/RepositoryNameConstrainValidator.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/RepositoryTypeConstraint.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/RepositoryTypeConstraintValidator.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/cli/RepositoryCommand.java create mode 100644 scm-core/src/main/resources/ValidationMessages.properties create mode 100644 scm-core/src/main/resources/ValidationMessages_de.properties create mode 100644 scm-core/src/test/java/sonia/scm/cli/CommandValidatorTest.java create mode 100644 scm-core/src/test/java/sonia/scm/cli/TemplateRendererTest.java create mode 100644 scm-core/src/test/java/sonia/scm/repository/RepositoryNameConstrainValidatorTest.java create mode 100644 scm-core/src/test/java/sonia/scm/repository/RepositoryTypeConstraintValidatorTest.java create mode 100644 scm-core/src/test/resources/sonia/scm/cli/i18n.properties create mode 100644 scm-core/src/test/resources/sonia/scm/cli/i18n_de.properties create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/CliResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/CliExceptionHandler.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/CliExitException.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/CliProcessor.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/CommandFactory.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/CommandRegistry.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/HelpMixin.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/JsonStreamingCliContext.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/LogoutCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/NonExistingParentCommandException.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/PingCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandCollector.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandNode.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/ScmManagerCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryCommandDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryCreateCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryDeleteCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryGetCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryListCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryModifyCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryTemplateRenderer.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/validation/DefaultValidatorProvider.java create mode 100644 scm-webapp/src/main/java/sonia/scm/validation/GuiceConstraintValidatorFactory.java create mode 100644 scm-webapp/src/main/java/sonia/scm/validation/ResteasyValidator.java create mode 100644 scm-webapp/src/main/java/sonia/scm/validation/ResteasyValidatorContextResolver.java create mode 100644 scm-webapp/src/main/java/sonia/scm/validation/ValidationModule.java create mode 100644 scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties create mode 100644 scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties create mode 100644 scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/cli/CommandRegistryTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/cli/JsonStreamingCliContextTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/cli/RegisteredCommandCollectorTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryCreateCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryDeleteCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryGetCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryListCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryModifyCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapperTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/validation/DefaultValidatorProviderTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/validation/GuiceConstraintValidatorFactoryTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/validation/RepositoryManagerModule.java create mode 100644 scm-webapp/src/test/java/sonia/scm/validation/RepositoryType.java create mode 100644 scm-webapp/src/test/java/sonia/scm/validation/RepositoryTypeConstraintValidator.java create mode 100644 scm-webapp/src/test/java/sonia/scm/validation/ValidationModuleTest.java create mode 100644 scm-webapp/src/test/resources/sonia/scm/cli/test.properties create mode 100644 scm-webapp/src/test/resources/sonia/scm/cli/test_de.properties diff --git a/docs/en/development/cli-guideline.md b/docs/en/development/cli-guideline.md new file mode 100644 index 0000000000..2aeaeaf92b --- /dev/null +++ b/docs/en/development/cli-guideline.md @@ -0,0 +1,139 @@ +# CLI Guidelines + +## Resource centered api +Every new command group starts with the resource name like `repo`. +Commands should be defined as singular. For repository commands it is `repo` and **not** `repos`. +You may set aliases to make your command more convenient to use. + +```java +@CommandLine.Command(name = "repo") +``` + +## Subcommands +Subcommands are action centered and can look like `scm repo create x/y`. + +The RepositoryCreateCommand is a subcommand of RepositoryCommand and must be explicitly annotated. +```java +@ParentCommand(value = RepositoryCommand.class) +``` + +## Parameters and options +Every required field for a command must be a parameter. All other fields have to be options. + +`scm repo create git namespace/name --init --description="test"` + +The repository `type` and `namespace/name` must be set, so they must be annotated as parameters. +The other fields like `init` and `description` are optional and are therefore annotated as options. +```java + @CommandLine.Parameters + private String type; + @CommandLine.Parameters + private String repository; + @CommandLine.Option(names = "--description") + private String description; + @CommandLine.Option(names = "--contact") + private String contact; + @CommandLine.Option(names = "--init") + private boolean init; +``` + +## Templating +Commands which return large texts or much content should allow templating. +This can be achieved by using the TemplateRenderer. +If you inject the TemplateRenderer you must annotate it as a Mixin: +```java + @CommandLine.Mixin + private final TemplateRenderer templateRenderer; +``` + +### Table +Besides "loose" templates, you can use a table-like template to render your output. +For this purpose use the TemplateRender and create table first. +Then add your table headers and rows. + +```java + Table table = templateRenderer.createTable(); + table.addHeader("repoName", "repoType", "repoUrl"); + for (RepositoryCommandDto dto : dtos) { + table.addRow(dto.getNamespace() + "/" + dto.getName(), dto.getType(), dto.getUrl()); + } + templateRenderer.renderToStdout(TABLE_TEMPLATE, ImmutableMap.of("rows", table, "repos", dtos)); +``` + +#### Result +```shell + NAME TYPE URL + scmadmin/nice_repo git http://localhost:8081/scm/repo/scmadmin/nice_repo +``` + +### Key/Value Table +To create a two column (key-value-style) table you can use the `addKeyValueRow()` method. + +```java + Table table = createTable(); + RepositoryCommandDto dto = mapper.map(repository); + table.addLabelValueRow("repoNamespace", dto.getNamespace()); + table.addLabelValueRow("repoName", dto.getName()); + table.addLabelValueRow("repoType", dto.getType()); + table.addLabelValueRow("repoContact", dto.getContact()); + table.addLabelValueRow("repoUrl", dto.getUrl()); + table.addLabelValueRow("repoDescription", dto.getDescription()); + renderToStdout(DETAILS_TABLE_TEMPLATE, ImmutableMap.of("rows", table, "repo", dto)); +``` + +#### Result + +```shell +Namespace: scmadmin +Name : testrepo +Type : git +``` + +## I18n +The CLI client commands should support multiple languages. +This can be done by using translation keys in the related resource bundles. +By default, we support English and German translations. + +### Example +```java + static final String DEFAULT_TEMPLATE = String.join("\n", + "{{repo.namespace}}/{{repo.name}}", + "{{i18n.repoDescription}}: {{repo.description}}", + "{{i18n.repoType}}: {{repo.type}}", + "{{i18n.repoContact}}: {{repo.contact}}" + ); +``` + +The variables starting with `i18n` are translations from the resource bundles. +The fields starting with `repo` are context related model data from the repository we are currently accessing. + +## Error handling +There are different options on how to handle errors. +You can use the TemplateRender and print the errors or exception messages to stderr channel. + +However, you also can throw an exception directly inside your execution. +These exceptions will be handled by the CliExceptionHandler and will be printed to the stderr channel based on a specific template. + +## Validation +The CLI commands support Java bean validation. +If you want to use this validation you have to inject the CommandValidator and call `validator.validate()` in the first line of the command execution. +Then you can simply annotate your fields with validation annotations. + +### Example +```java + @Email + @CommandLine.Option(names = {"--contact", "-c"}) + private String contact; +``` + +```java + @Inject + public MyCommand(CommandValidator validator) { + this.validator = validator; + } + +@Override +public void run() { + validator.validate(); + ... +} diff --git a/gradle/changelog/cli.yaml b/gradle/changelog/cli.yaml new file mode 100644 index 0000000000..86a21aa41d --- /dev/null +++ b/gradle/changelog/cli.yaml @@ -0,0 +1,2 @@ +- type: added + description: Add cli support with repository actions ([#1987](https://github.com/scm-manager/scm-manager/pull/1987)) diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index d7959064ef..a7ff616187 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -54,6 +54,9 @@ ext { resteasyServletInitializer: "org.jboss.resteasy:resteasy-servlet-initializer:${resteasyVersion}", resteasyValidatorProvider: "org.jboss.resteasy:resteasy-validator-provider:${resteasyVersion}", + // cli + picocli: 'info.picocli:picocli:4.6.3', + // json jacksonCore: "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}", jacksonAnnotations: "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}", @@ -164,6 +167,9 @@ ext { junitVintageEngine: "org.junit.vintage:junit-vintage-engine:${junitJupiterVersion}", junit: 'junit:junit:4.13.1', + // junit 5 extensions + junitPioneer: 'org.junit-pioneer:junit-pioneer:1.6.2', + // assertions hamcrestCore: "org.hamcrest:hamcrest-core:${hamcrestVersion}", hamcrestLibrary: "org.hamcrest:hamcrest-library:${hamcrestVersion}", diff --git a/scm-annotation-processor/build.gradle b/scm-annotation-processor/build.gradle index 721577db36..bf0ebc7281 100644 --- a/scm-annotation-processor/build.gradle +++ b/scm-annotation-processor/build.gradle @@ -37,6 +37,9 @@ dependencies { // rest api implementation libraries.jaxRs + // cli + implementation libraries.picocli + // mapper implementation libraries.mapstruct diff --git a/scm-annotation-processor/gradle.lockfile b/scm-annotation-processor/gradle.lockfile index 7e513f2ec6..85fd3d7b6f 100644 --- a/scm-annotation-processor/gradle.lockfile +++ b/scm-annotation-processor/gradle.lockfile @@ -10,6 +10,7 @@ com.google.guava:guava:30.1-jre=compileClasspath,compileClasspathCopy,default,de com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.inject:guice:5.0.1=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.j2objc:j2objc-annotations:1.3=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +info.picocli:picocli:4.6.3=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy javax.inject:javax.inject:1=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy javax.ws.rs:javax.ws.rs-api:2.1.1=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.checkerframework:checker-qual:3.5.0=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy diff --git a/scm-annotation-processor/src/main/java/sonia/scm/annotation/ScmAnnotationProcessor.java b/scm-annotation-processor/src/main/java/sonia/scm/annotation/ScmAnnotationProcessor.java index ca7ad63f86..bedb506468 100644 --- a/scm-annotation-processor/src/main/java/sonia/scm/annotation/ScmAnnotationProcessor.java +++ b/scm-annotation-processor/src/main/java/sonia/scm/annotation/ScmAnnotationProcessor.java @@ -34,6 +34,7 @@ import org.mapstruct.Mapper; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.xml.sax.SAXException; +import picocli.CommandLine; import sonia.scm.annotation.ClassSetElement.ClassWithAttributes; import sonia.scm.plugin.PluginAnnotation; import sonia.scm.plugin.Requires; @@ -98,7 +99,8 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { private static final Set CLASS_ANNOTATIONS = ImmutableSet.of(new ClassAnnotation("rest-resource", Path.class), new ClassAnnotation("rest-provider", Provider.class), - new ClassAnnotation("mapper", Mapper.class)); + new ClassAnnotation("mapper", Mapper.class), + new ClassAnnotation("cli-command", CommandLine.Command.class)); @Override public boolean process(Set annotations, @@ -144,7 +146,8 @@ public final class ScmAnnotationProcessor extends AbstractProcessor { TypeElement annotation = null; for (TypeElement typeElement : annotations) { - if (typeElement.getQualifiedName().toString().equals(annotationClass.getName())) { + // Replace $ with . to match subclasses + if (typeElement.getQualifiedName().toString().equals(annotationClass.getName().replace("$", "."))) { annotation = typeElement; break; diff --git a/scm-annotations/src/main/java/sonia/scm/cli/ParentCommand.java b/scm-annotations/src/main/java/sonia/scm/cli/ParentCommand.java new file mode 100644 index 0000000000..4e5eaa7c0d --- /dev/null +++ b/scm-annotations/src/main/java/sonia/scm/cli/ParentCommand.java @@ -0,0 +1,36 @@ +/* + * 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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ParentCommand { + Class value(); +} diff --git a/scm-core/build.gradle b/scm-core/build.gradle index f555b6a56b..c56e7dfc57 100644 --- a/scm-core/build.gradle +++ b/scm-core/build.gradle @@ -65,6 +65,9 @@ dependencies { api libraries.jaxRs testImplementation libraries.resteasyCore + // cli + api libraries.picocli + // json api libraries.jacksonCore api libraries.jacksonAnnotations @@ -109,6 +112,7 @@ dependencies { testImplementation libraries.junitJupiterApi testImplementation libraries.junitJupiterParams testRuntimeOnly libraries.junitJupiterEngine + testImplementation libraries.junitPioneer // shiro testImplementation libraries.shiroExtension diff --git a/scm-core/gradle.lockfile b/scm-core/gradle.lockfile index b1e72b7f44..8ee4010d11 100644 --- a/scm-core/gradle.lockfile +++ b/scm-core/gradle.lockfile @@ -32,6 +32,7 @@ commons-beanutils:commons-beanutils:1.9.4=annotationProcessor,annotationProcesso commons-collections:commons-collections:3.2.2=annotationProcessor,annotationProcessorCopy,compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy commons-lang:commons-lang:2.6=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy de.otto.edison:edison-hal:2.1.0=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +info.picocli:picocli:4.6.3=annotationProcessor,annotationProcessorCopy,compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.micrometer:micrometer-core:1.6.4=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.smallrye.common:smallrye-common-annotation:1.6.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.smallrye.common:smallrye-common-classloader:1.6.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy @@ -85,13 +86,21 @@ org.jboss.resteasy:resteasy-core:4.7.5.Final=testCompileClasspath,testCompileCla org.jboss.spec.javax.annotation:jboss-annotations-api_1.3_spec:2.0.1.Final=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.1_spec:2.0.1.Final=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.jboss.spec.javax.xml.bind:jboss-jaxb-api_2.3_spec:2.0.0.Final=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy -org.junit.jupiter:junit-jupiter-api:5.7.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy -org.junit.jupiter:junit-jupiter-engine:5.7.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy -org.junit.jupiter:junit-jupiter-params:5.7.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy -org.junit.platform:junit-platform-commons:1.7.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy -org.junit.platform:junit-platform-engine:1.7.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy -org.junit.vintage:junit-vintage-engine:5.7.0=testRuntimeClasspath,testRuntimeClasspathCopy -org.junit:junit-bom:5.7.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit-pioneer:junit-pioneer:1.6.2=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.jupiter:junit-jupiter-api:5.7.0=testCompileClasspath,testCompileClasspathCopy +org.junit.jupiter:junit-jupiter-api:5.7.2=testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.jupiter:junit-jupiter-engine:5.7.0=testCompileClasspath,testCompileClasspathCopy +org.junit.jupiter:junit-jupiter-engine:5.7.2=testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.jupiter:junit-jupiter-params:5.7.0=testCompileClasspath,testCompileClasspathCopy +org.junit.jupiter:junit-jupiter-params:5.7.2=testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.platform:junit-platform-commons:1.7.0=testCompileClasspath,testCompileClasspathCopy +org.junit.platform:junit-platform-commons:1.7.2=testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.platform:junit-platform-engine:1.7.0=testCompileClasspath,testCompileClasspathCopy +org.junit.platform:junit-platform-engine:1.7.2=testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.platform:junit-platform-launcher:1.7.2=testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.vintage:junit-vintage-engine:5.7.2=testRuntimeClasspath,testRuntimeClasspathCopy +org.junit:junit-bom:5.7.0=testCompileClasspath,testCompileClasspathCopy +org.junit:junit-bom:5.7.2=testRuntimeClasspath,testRuntimeClasspathCopy org.latencyutils:LatencyUtils:2.0.3=default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.mapstruct:mapstruct-jdk8:1.3.1.Final=annotationProcessor,annotationProcessorCopy,compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.mapstruct:mapstruct-processor:1.3.1.Final=annotationProcessor,annotationProcessorCopy diff --git a/scm-core/src/main/java/sonia/scm/cli/CliContext.java b/scm-core/src/main/java/sonia/scm/cli/CliContext.java new file mode 100644 index 0000000000..ea79ec900c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/cli/CliContext.java @@ -0,0 +1,68 @@ +/* + * 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.io.InputStream; +import java.io.PrintWriter; +import java.util.Locale; + +/** + * Context for the CLI client which is used by the CLI commands + * @since 2.33.0 + */ +public interface CliContext { + /** + * This is the {@link PrintWriter} which writes to the stdout channel of the client terminal. + * Use this channel for "normal" messages, for errors use {@link CliContext#getStderr()}. + * @return writer for stdout + */ + PrintWriter getStdout(); + + /** + * This is the {@link PrintWriter} which writes to the stderr channel of the client terminal. + * Use this channel for error messages, for "normal" messages use {@link CliContext#getStdout()}. + * @return writer for stderr + */ + PrintWriter getStderr(); + + /** + * Returns an {@link InputStream} which represents the stdin of the client terminal. + * @return the stdin channel of the client terminal + */ + InputStream getStdin(); + + /** + * Sets the exit code for the current command execution and stops the execution. + * @param exitcode exit code which will be return to the client terminal + * @see {@link ExitCode} + */ + void exit(int exitcode); + + /** + * Returns the {@link Locale} of the client terminal. + * @return locale of the client terminal + */ + Locale getLocale(); +} diff --git a/scm-core/src/main/java/sonia/scm/cli/CliException.java b/scm-core/src/main/java/sonia/scm/cli/CliException.java new file mode 100644 index 0000000000..0b827bd2dc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/cli/CliException.java @@ -0,0 +1,41 @@ +/* + * 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; + +/** + * Parent class for command line exceptions. + * @since 2.33.0 + */ +public class CliException extends RuntimeException { + + public CliException(String message) { + super(message); + } + + public CliException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/cli/CommandValidator.java b/scm-core/src/main/java/sonia/scm/cli/CommandValidator.java new file mode 100644 index 0000000000..f8a9b9af78 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/cli/CommandValidator.java @@ -0,0 +1,117 @@ +/* + * 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 picocli.CommandLine; + +import javax.inject.Inject; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.ConstraintViolation; +import javax.validation.MessageInterpolator; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.Set; + +/** + * This the command validator which should be used to validate CLI commands with Bean validation. + * @see Bean validation spec + * @since 2.33.0 + */ +// We need to hide this because it is not a real command but a mixin. +// The command annotation is required for picocli to resolve this properly. +public final class CommandValidator { + + private final CliContext context; + private final Validator validator; + + @CommandLine.Spec(CommandLine.Spec.Target.MIXEE) + private CommandLine.Model.CommandSpec spec; + + // We need an option to trick PicoCli into accepting our mixin + @CommandLine.Option(names = "--hidden-flag", hidden = true) + private boolean hiddenFlag ; + + @Inject + public CommandValidator(CliContext context, ConstraintValidatorFactory constraintValidatorFactory) { + this.context = context; + this.validator = createValidator(constraintValidatorFactory); + } + + private Validator createValidator(ConstraintValidatorFactory constraintValidatorFactory) { + ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); + + return validatorFactory.usingContext() + .constraintValidatorFactory(constraintValidatorFactory) + .messageInterpolator(new LocaleSpecificMessageInterpolator(validatorFactory.getMessageInterpolator(), context.getLocale())) + .getValidator(); + } + + /** + * Execute validation and exit the command on validation failure + */ + public void validate() { + Set> violations = validator.validate(spec.userObject()); + + if (!violations.isEmpty()) { + StringBuilder errorMsg = new StringBuilder(); + for (ConstraintViolation violation : violations) { + errorMsg.append(evaluateErrorTemplate()).append(violation.getMessage()).append("\n"); + } + throw new CommandLine.ParameterException(spec.commandLine(), errorMsg.toString()); + } + } + + private String evaluateErrorTemplate() { + ResourceBundle bundle = spec.resourceBundle(); + if (bundle != null && bundle.containsKey("errorLabel")) { + return bundle.getString("errorLabel") + ": "; + } + return "ERROR: "; + } + + private static class LocaleSpecificMessageInterpolator implements MessageInterpolator { + + private final MessageInterpolator defaultMessageInterpolator; + private final Locale defaultLocale; + + private LocaleSpecificMessageInterpolator(MessageInterpolator defaultMessageInterpolator, Locale defaultLocale) { + this.defaultMessageInterpolator = defaultMessageInterpolator; + this.defaultLocale = defaultLocale; + } + + @Override + public String interpolate(String messageTemplate, Context context) { + return interpolate(messageTemplate, context, defaultLocale); + } + + @Override + public String interpolate(String messageTemplate, Context context, Locale locale) { + return defaultMessageInterpolator.interpolate(messageTemplate, context, locale); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/cli/ExitCode.java b/scm-core/src/main/java/sonia/scm/cli/ExitCode.java new file mode 100644 index 0000000000..5931c17d40 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/cli/ExitCode.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * @see picocli.CommandLine.ExitCode + * @since 2.33.0 + */ +public final class ExitCode { + + public static final int OK = 0; + public static final int SERVER_ERROR = 1; + public static final int USAGE = 2; + public static final int NOT_FOUND = 3; + + private ExitCode() {} +} diff --git a/scm-core/src/main/java/sonia/scm/cli/Table.java b/scm-core/src/main/java/sonia/scm/cli/Table.java new file mode 100644 index 0000000000..9a66a10437 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/cli/Table.java @@ -0,0 +1,175 @@ +/* + * 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 com.google.common.base.Strings; +import lombok.Value; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * This table can be used to display table-like command output + * @since 2.33.0 + */ +public final class Table implements Iterable { + + private static final String DEFAULT_LABEL_VALUE_SEPARATOR = ": "; + + private final List data = new ArrayList<>(); + + @Nullable + private final ResourceBundle bundle; + + Table(@Nullable ResourceBundle bundle) { + this.bundle = bundle; + } + + /** + * Sets the table headers. + * You can use resource keys which will be translated using the related resource bundle. + * @param keys actual names or resource keys for your table header + */ + public void addHeader(String... keys) { + data.add(Arrays.stream(keys).map(this::getLocalizedValue).toArray(String[]::new)); + } + + /** + * Add a single row of values to the table. + * @param row values for a single table row + */ + public void addRow(String... row) { + data.add(row); + } + + /** + * Creates a table entry with two columns separated by {@link #DEFAULT_LABEL_VALUE_SEPARATOR}. + * @param label label for the left table column + * @param value value for the right table column + */ + public void addLabelValueRow(String label, String value) { + addLabelValueRow(label, value, DEFAULT_LABEL_VALUE_SEPARATOR); + } + + /** + * Creates a table entry with two columns separated by the given separator. + * @param label label for the left table column + * @param value value for the right table column + * @param separator separator used to separate the label from the value + */ + public void addLabelValueRow(String label, String value, String separator) { + addRow(getLocalizedValue(label) + separator, value); + } + + /** + * Returns a list of the table rows. + * This is required for the internal table implementation. + * @return a list of the table rows + */ + public List getRows() { + Map maxLength = calculateMaxLength(); + + List rows = new ArrayList<>(); + for (int r = 0; r < data.size(); r++) { + String[] rowArray = data.get(r); + + List cells = new ArrayList<>(); + Row row = new Row(r == 0, r + 1 == data.size(), r, cells); + for (int c = 0; c < rowArray.length; c++) { + String value = createValueWithLength(Strings.nullToEmpty(rowArray[c]), maxLength.get(c)); + Cell cell = new Cell(row, c == 0, c + 1 == rowArray.length, c, value); + cells.add(cell); + } + rows.add(row); + } + + return rows; + } + + private String getLocalizedValue(String key) { + if (bundle != null && bundle.containsKey(key)) { + return bundle.getString(key); + } + return key; + } + + private String createValueWithLength(String value, int length) { + if (value.length() < length) { + StringBuilder builder = new StringBuilder(value); + for (int j = value.length(); j < length; j++) { + builder.append(" "); + } + return builder.toString(); + } + return value; + } + + private Map calculateMaxLength() { + Map maxLength = new HashMap<>(); + for (String[] row : data) { + for (int i = 0; i < row.length; i++) { + int currentMaxLength = maxLength.getOrDefault(i, 0); + String col = row[i]; + int length = col != null ? col.length() : 0; + if (length > currentMaxLength) { + maxLength.put(i, length); + } + } + } + return maxLength; + } + + @Override + public Iterator iterator() { + return getRows().iterator(); + } + + @Value + public class Cell { + + Row row; + boolean first; + boolean last; + int index; + String value; + + } + + @Value + public class Row { + + boolean first; + boolean last; + int index; + List cols; + + } +} diff --git a/scm-core/src/main/java/sonia/scm/cli/TemplateRenderer.java b/scm-core/src/main/java/sonia/scm/cli/TemplateRenderer.java new file mode 100644 index 0000000000..4cb4eb0dce --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/cli/TemplateRenderer.java @@ -0,0 +1,166 @@ +/* + * 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 com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import picocli.CommandLine; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import javax.inject.Inject; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.function.UnaryOperator; + +/** + * This is the default template renderer which should be used to write templated content to the channels of the CLI connection. + * @since 2.33.0 + */ +public class TemplateRenderer { + + @CommandLine.Option(names = {"--template", "-t"}, paramLabel = "TEMPLATE", descriptionKey = "scm.templateRenderer.template") + private String template; + + private static final String DEFAULT_ERROR_TEMPLATE = String.join("\n", + "{{i18n.errorCommandFailed}}", "{{i18n.errorUnknownError}}:", + "{{error}}" + ); + + private final CliContext context; + private final TemplateEngine templateEngine; + @CommandLine.Spec(CommandLine.Spec.Target.MIXEE) + private CommandLine.Model.CommandSpec spec; + + @Inject + public TemplateRenderer(CliContext context, TemplateEngineFactory templateEngineFactory) { + this.context = context; + this.templateEngine = templateEngineFactory.getDefaultEngine(); + } + + /** + * Writes templated content to the stdout channel + * @param template the mustache template + * @param model the model which should be used for templating + */ + public void renderToStdout(String template, Map model) { + exec(context.getStdout(), template, model); + } + + /** + * Writes templated content to the stderr channel + * @param template the mustache template + * @param model the model which should be used for templating + */ + public void renderToStderr(String template, Map model) { + exec(context.getStderr(), template, model); + } + + /** + * Writes an error to the stderr channel using the default error template + * @param error the error which should be used for templating + */ + public void renderDefaultError(String error) { + exec(context.getStderr(), DEFAULT_ERROR_TEMPLATE, ImmutableMap.of("error", error)); + } + + /** + * Writes the exception message to the stderr channel using the default error template + * @param exception the exception which should be used for templating + */ + public void renderDefaultError(Exception exception) { + renderDefaultError(exception.getMessage()); + } + + /** + * Creates the table which should be used to template table-like content. + * @return table for templating content + */ + public Table createTable() { + return new Table(spec.resourceBundle()); + } + + private void exec(PrintWriter stream, String defaultTemplate, Map model) { + try { + Template tpl = templateEngine.getTemplate(getClass().getName(), new StringReader(MoreObjects.firstNonNull(template, defaultTemplate))); + tpl.execute(stream, createModel(model)); + stream.flush(); + } catch (IOException e) { + throw new TemplateRenderingException("failed to render template", e); + } + } + + private Object createModel(Map model) { + Map finalModel = new HashMap<>(model); + finalModel.put("lf", "\n"); + UnaryOperator upper = value -> value.toUpperCase(context.getLocale()); + finalModel.put("upper", upper); + + ResourceBundle resourceBundle = spec.resourceBundle(); + if (resourceBundle != null) { + finalModel.put("i18n", new I18n(resourceBundle)); + } + return Collections.unmodifiableMap(finalModel); + } + + @VisibleForTesting + void setSpec(CommandLine.Model.CommandSpec spec) { + this.spec = spec; + } + + @SuppressWarnings("java:S2160") // Do not need equals or hashcode + private static class I18n extends AbstractMap { + + private final ResourceBundle resourceBundle; + + I18n(ResourceBundle resourceBundle) { + this.resourceBundle = resourceBundle; + } + + @Override + public String get(Object key) { + return resourceBundle.getString(key.toString()); + } + + @Override + public boolean containsKey(Object key) { + return resourceBundle.containsKey(key.toString()); + } + + @Override + public Set> entrySet() { + throw new UnsupportedOperationException("Should not be used"); + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/cli/TemplateRenderingException.java b/scm-core/src/main/java/sonia/scm/cli/TemplateRenderingException.java new file mode 100644 index 0000000000..a00a3210bc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/cli/TemplateRenderingException.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * Exception is thrown if {@link TemplateRenderer} could not render the template. + * @since 2.33.0 + */ +public class TemplateRenderingException extends CliException { + + public TemplateRenderingException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/NamedClassElement.java b/scm-core/src/main/java/sonia/scm/plugin/NamedClassElement.java new file mode 100644 index 0000000000..40c00c7d4e --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/NamedClassElement.java @@ -0,0 +1,49 @@ +/* + * 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.plugin; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.HashSet; + +@Getter +@ToString +@EqualsAndHashCode(callSuper = true) +@XmlAccessorType(XmlAccessType.FIELD) +@NoArgsConstructor(access = AccessLevel.PACKAGE) +public class NamedClassElement extends ClassElement { + private String name; + + public NamedClassElement(String name, String clazz) { + super(clazz, null, new HashSet<>()); + this.name = name; + } +} diff --git a/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java b/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java index 7038ff7251..d4796d9d3e 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java +++ b/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java @@ -58,6 +58,9 @@ public class ScmModule { @XmlElement(name = "rest-resource") private Set restResources; + @XmlElement(name = "cli-command") + private Set cliCommands; + @XmlElement(name = "mapper") private Set mappers; @@ -87,6 +90,10 @@ public class ScmModule { return nonNull(restResources); } + public Iterable getCliCommands() { + return nonNull(cliCommands); + } + public Iterable getMappers() { return nonNull(mappers); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryName.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryName.java new file mode 100644 index 0000000000..3ce550ca3b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryName.java @@ -0,0 +1,75 @@ +/* + * 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.repository; + +import javax.validation.Constraint; +import javax.validation.Payload; +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; + +/** + * Validates the name of a repository. + * @since 2.33.0 + */ +@Documented +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = RepositoryNameConstrainValidator.class) +public @interface RepositoryName { + + String message() default "{sonia.scm.repository.RepositoryName.message}"; + Class[] groups() default { }; + Class[] payload() default { }; + + /** + * Specify namespace prefix validation. Default is {@link Namespace#NONE}. + * + * @return namespace validation + */ + Namespace namespace() default Namespace.NONE; + + /** + * Options to control the namespace prefix validation. + */ + enum Namespace { + /** + * The repository name does not contain a namespace prefix. + */ + NONE, + + /** + * The repository name can contain a namespace prefix. + */ + OPTIONAL, + + /** + * The repository name must start with a namespace prefix. + */ + REQUIRED; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryNameConstrainValidator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNameConstrainValidator.java new file mode 100644 index 0000000000..91a6474436 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryNameConstrainValidator.java @@ -0,0 +1,60 @@ +/* + * 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.repository; + +import sonia.scm.util.ValidationUtil; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class RepositoryNameConstrainValidator implements ConstraintValidator { + + private RepositoryName.Namespace namespace; + + @Override + public void initialize(RepositoryName constraintAnnotation) { + namespace = constraintAnnotation.namespace(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + String[] parts = value.split("/"); + if (namespace == RepositoryName.Namespace.REQUIRED) { + if (parts.length == 2) { + return ValidationUtil.isRepositoryNameValid(parts[1]); + } + return false; + } else if (namespace == RepositoryName.Namespace.OPTIONAL) { + if (parts.length == 2) { + return ValidationUtil.isRepositoryNameValid(parts[1]); + } else if (parts.length == 1) { + return ValidationUtil.isRepositoryNameValid(parts[0]); + } else { + return false; + } + } + return ValidationUtil.isRepositoryNameValid(value); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryTypeConstraint.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryTypeConstraint.java new file mode 100644 index 0000000000..ec2c8988e3 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryTypeConstraint.java @@ -0,0 +1,48 @@ +/* + * 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.repository; + +import javax.validation.Constraint; +import javax.validation.Payload; +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; + +/** + * Validates the type of repository. Only configured and enabled repository types are valid. + * + * @since 2.33.0 + */ +@Documented +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = RepositoryTypeConstraintValidator.class) +public @interface RepositoryTypeConstraint { + String message() default "{sonia.scm.repository.RepositoryTypeConstraint.message}"; + Class[] groups() default { }; + Class[] payload() default { }; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryTypeConstraintValidator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryTypeConstraintValidator.java new file mode 100644 index 0000000000..4b7f1d4ded --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryTypeConstraintValidator.java @@ -0,0 +1,78 @@ +/* + * 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.repository; + +import sonia.scm.Type; + +import javax.inject.Inject; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.stream.Collectors; + +/** + * Validator for {@link RepositoryTypeConstraint}. + * + * @since 2.33.0 + */ +public class RepositoryTypeConstraintValidator implements ConstraintValidator { + + private final RepositoryManager repositoryManager; + + @Inject + public RepositoryTypeConstraintValidator(RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + } + + public RepositoryManager getRepositoryManager() { + return repositoryManager; + } + + @Override + public boolean isValid(String type, ConstraintValidatorContext context) { + if (!isSupportedType(type)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(createMessage(context)).addConstraintViolation(); + return false; + } + return true; + } + + private boolean isSupportedType(String type) { + return repositoryManager.getConfiguredTypes() + .stream().anyMatch(t -> t.getName().equalsIgnoreCase(type)); + } + + private String createMessage(ConstraintValidatorContext context) { + String message = context.getDefaultConstraintMessageTemplate(); + return message + " " + commaSeparatedTypes(); + } + + private String commaSeparatedTypes() { + return repositoryManager.getConfiguredTypes() + .stream() + .map(Type::getName) + .collect(Collectors.joining(", ")); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/cli/RepositoryCommand.java b/scm-core/src/main/java/sonia/scm/repository/cli/RepositoryCommand.java new file mode 100644 index 0000000000..c15630af7a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/cli/RepositoryCommand.java @@ -0,0 +1,30 @@ +/* + * 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.repository.cli; + +import picocli.CommandLine; + +@CommandLine.Command(name = "repo") +public class RepositoryCommand {} diff --git a/scm-core/src/main/resources/ValidationMessages.properties b/scm-core/src/main/resources/ValidationMessages.properties new file mode 100644 index 0000000000..6c5fd41959 --- /dev/null +++ b/scm-core/src/main/resources/ValidationMessages.properties @@ -0,0 +1,26 @@ +# +# 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. +# + +sonia.scm.repository.RepositoryTypeConstraint.message = Invalid repository type, please use one of the following: +sonia.scm.repository.RepositoryName.message = Invalid repository name diff --git a/scm-core/src/main/resources/ValidationMessages_de.properties b/scm-core/src/main/resources/ValidationMessages_de.properties new file mode 100644 index 0000000000..e3ec9b0c75 --- /dev/null +++ b/scm-core/src/main/resources/ValidationMessages_de.properties @@ -0,0 +1,26 @@ +# +# 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. +# + +sonia.scm.repository.RepositoryTypeConstraint.message = Ungültiger Repository-Typ, bitte verwenden Sie einen der folgenden: +sonia.scm.repository.RepositoryName.message = Ungültiger Repository Name diff --git a/scm-core/src/test/java/sonia/scm/cli/CommandValidatorTest.java b/scm-core/src/test/java/sonia/scm/cli/CommandValidatorTest.java new file mode 100644 index 0000000000..f71d4fe473 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/cli/CommandValidatorTest.java @@ -0,0 +1,126 @@ +/* + * 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 org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import picocli.CommandLine; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.constraints.Email; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Locale; +import java.util.ResourceBundle; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CommandValidatorTest { + + @Mock + private CliContext context; + + @Test + void shouldValidateCommand() { + ResourceBundle resourceBundle = ResourceBundle.getBundle("sonia.scm.cli.i18n", Locale.ENGLISH); + when(context.getLocale()).thenReturn(Locale.ENGLISH); + CommandLine commandLine = new CommandLine(Command.class, new TestingCommandFactory()); + commandLine.setResourceBundle(resourceBundle); + StringWriter stringWriter = new StringWriter(); + commandLine.setErr(new PrintWriter(stringWriter)); + + commandLine.execute("--mail=test"); + + assertThat(stringWriter.toString()).contains("ERROR: must be a well-formed email address"); + } + + @Test + void shouldValidateCommandWithGermanLocale() { + ResourceBundle resourceBundle = ResourceBundle.getBundle("sonia.scm.cli.i18n", Locale.GERMAN); + when(context.getLocale()).thenReturn(Locale.GERMAN); + CommandLine commandLine = new CommandLine(Command.class, new TestingCommandFactory()); + commandLine.setResourceBundle(resourceBundle); + StringWriter stringWriter = new StringWriter(); + commandLine.setErr(new PrintWriter(stringWriter)); + + commandLine.execute("--mail=test"); + + assertThat(stringWriter.toString()).contains("FEHLER: muss eine korrekt formatierte E-Mail-Adresse sein"); + } + + @CommandLine.Command + public static class Command implements Runnable { + + @CommandLine.Mixin + private CommandValidator commandValidator; + + @Email + @CommandLine.Option(names = "--mail") + private String mail; + + public Command(CliContext context) { + commandValidator = new CommandValidator(context, new TestingConstraintValidatorFactory()); + } + + @Override + public void run() { + commandValidator.validate(); + } + } + + class TestingCommandFactory implements CommandLine.IFactory { + + @Override + public K create(Class cls) throws Exception { + try { + return cls.getConstructor(CliContext.class).newInstance(context); + } catch (Exception e) { + return CommandLine.defaultFactory().create(cls); + } + } + } + + static class TestingConstraintValidatorFactory implements ConstraintValidatorFactory { + + @Override + public > T getInstance(Class key) { + try { + return key.getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException("Failed to create constraint validator"); + } + } + + @Override + public void releaseInstance(ConstraintValidator instance) { + + } + } +} diff --git a/scm-core/src/test/java/sonia/scm/cli/TemplateRendererTest.java b/scm-core/src/test/java/sonia/scm/cli/TemplateRendererTest.java new file mode 100644 index 0000000000..8fbfadf49d --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/cli/TemplateRendererTest.java @@ -0,0 +1,109 @@ +/* + * 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 com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import picocli.CommandLine; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TemplateRendererTest { + + @Mock + private CliContext context; + @Mock + private TemplateEngineFactory templateEngineFactory; + @Mock + private TemplateEngine engine; + @Mock + private Template template; + + @Test + void shouldTemplateContentToStdout() throws IOException { + when(context.getStdout()).thenReturn(new PrintWriter(new StringWriter())); + when(templateEngineFactory.getDefaultEngine()).thenReturn(engine); + when(engine.getTemplate(any(), any(StringReader.class))).thenReturn(template); + TemplateRenderer templateRenderer = new TemplateRenderer(context, templateEngineFactory); + templateRenderer.setSpec(CommandLine.Model.CommandSpec.create()); + + templateRenderer.renderToStdout(":{{test}}!", ImmutableMap.of("test", "test_output")); + + verify(template).execute(any(), any()); + } + + @Test + void shouldRenderErrorToStderr() throws IOException { + when(context.getStderr()).thenReturn(new PrintWriter(new StringWriter())); + when(templateEngineFactory.getDefaultEngine()).thenReturn(engine); + when(engine.getTemplate(any(), any(StringReader.class))).thenReturn(template); + TemplateRenderer templateRenderer = new TemplateRenderer(context, templateEngineFactory); + templateRenderer.setSpec(CommandLine.Model.CommandSpec.create()); + + templateRenderer.renderToStderr(":{{error}}!", ImmutableMap.of("error", "testerror")); + + verify(template).execute(any(), any()); + } + + @Test + void shouldRenderDefaultErrorToStderr() throws IOException { + when(context.getStderr()).thenReturn(new PrintWriter(new StringWriter())); + when(templateEngineFactory.getDefaultEngine()).thenReturn(engine); + when(engine.getTemplate(any(), any(StringReader.class))).thenReturn(template); + TemplateRenderer templateRenderer = new TemplateRenderer(context, templateEngineFactory); + templateRenderer.setSpec(CommandLine.Model.CommandSpec.create()); + + templateRenderer.renderDefaultError("testerror"); + + verify(template).execute(any(), any()); + } + + @Test + void shouldRenderExceptionToStderr() throws IOException { + when(context.getStderr()).thenReturn(new PrintWriter(new StringWriter())); + when(templateEngineFactory.getDefaultEngine()).thenReturn(engine); + when(engine.getTemplate(any(), any(StringReader.class))).thenReturn(template); + TemplateRenderer templateRenderer = new TemplateRenderer(context, templateEngineFactory); + templateRenderer.setSpec(CommandLine.Model.CommandSpec.create()); + + templateRenderer.renderDefaultError(new RuntimeException("test")); + + verify(template).execute(any(), any()); + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryNameConstrainValidatorTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryNameConstrainValidatorTest.java new file mode 100644 index 0000000000..b20309d3bc --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryNameConstrainValidatorTest.java @@ -0,0 +1,162 @@ +/* + * 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.repository; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.DefaultLocale; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.repository.RepositoryName.Namespace.OPTIONAL; +import static sonia.scm.repository.RepositoryName.Namespace.REQUIRED; + +class RepositoryNameConstrainValidatorTest { + + @Nested + class WithoutNamespace { + + @Test + void shouldPassValidation() { + assertThat(validate(new NamespaceNone("scm-manager"))).isEmpty(); + } + + @Test + @DefaultLocale("en") + void shouldFailValidation() { + assertThat(validate(new NamespaceNone("scm\\manager"))).hasSize(1).allSatisfy( + violation -> assertThat(violation.getMessage()).isEqualTo("Invalid repository name") + ); + } + + @Test + @DefaultLocale("de") + void shouldFailValidationWithGermanMessage() { + assertThat(validate(new NamespaceNone("scm\\manager"))).hasSize(1).allSatisfy( + violation -> assertThat(violation.getMessage()).isEqualTo("Ungültiger Repository Name") + ); + } + + @Test + @DefaultLocale("en") + void shouldFailWithSlashAndDisabledWithNamespaceOption() { + assertThat(validate(new NamespaceNone("scm/manager"))).isNotEmpty(); + } + + } + + @Nested + class WithRequiredNamespace { + + @Test + void shouldPassValidation() { + assertThat(validate(new RequiredNamespace("scm/manager"))).isEmpty(); + } + + @Test + void shouldFailWithMoreThanOneSlash() { + assertThat(validate(new RequiredNamespace("scm/mana/ger"))).isNotEmpty(); + } + + @Test + void shouldFailWithOutNamespace() { + assertThat(validate(new RequiredNamespace("scm-manager"))).isNotEmpty(); + } + + @Test + void shouldFailWithInvalidName() { + assertThat(validate(new RequiredNamespace("scm/ma\\nager"))).isNotEmpty(); + } + + } + + @Nested + class WithOptionalNamespace { + + @Test + void shouldPassValidationWithoutNamespace() { + assertThat(validate(new OptionalNamespace("scm-manager"))).isEmpty(); + } + + @Test + void shouldPassValidationWithNamespace() { + assertThat(validate(new OptionalNamespace("scm/manager"))).isEmpty(); + } + + @Test + void shouldFailWithMoreThanOneSlash() { + assertThat(validate(new OptionalNamespace("scm/mana/ger"))).isNotEmpty(); + } + + @Test + void shouldFailWithInvalidName() { + assertThat(validate(new OptionalNamespace("scm/ma\\nager"))).isNotEmpty(); + } + + } + + private Set> validate(T object) { + return validator().validate(object); + } + + private Validator validator() { + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + public static class NamespaceNone { + + @RepositoryName + private final String name; + + public NamespaceNone(String name) { + this.name = name; + } + } + + public static class RequiredNamespace { + + @RepositoryName(namespace = REQUIRED) + private final String name; + + public RequiredNamespace(String name) { + this.name = name; + } + } + + public static class OptionalNamespace { + + @RepositoryName(namespace = OPTIONAL) + private final String name; + + public OptionalNamespace(String name) { + this.name = name; + } + } + +} diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryTypeConstraintValidatorTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryTypeConstraintValidatorTest.java new file mode 100644 index 0000000000..9bbf9a4258 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryTypeConstraintValidatorTest.java @@ -0,0 +1,152 @@ +/* + * 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.repository; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.DefaultLocale; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryTypeConstraintValidatorTest { + + @Mock + private RepositoryManager repositoryManager; + + @Test + @DefaultLocale("en") + void shouldFailValidation() { + mockRepositoryTypes("git", "hg"); + + Validator validator = validator(); + Set> violations = validator.validate(new Repo("svn")); + assertThat(violations).hasSize(1).allSatisfy( + violation -> assertThat(violation.getMessage()).contains("Invalid repository type") + ); + } + + @Test + @DefaultLocale("de") + void shouldFailValidationWithGermanMessage() { + mockRepositoryTypes("svn", "hg"); + + Validator validator = validator(); + Set> violations = validator.validate(new Repo("git")); + assertThat(violations).hasSize(1).allSatisfy( + violation -> assertThat(violation.getMessage()).contains("Ungültiger Repository-Typ") + ); + } + + @Test + @DefaultLocale("en") + void shouldPassValidation() { + mockRepositoryTypes("svn", "git", "hg"); + + Validator validator = validator(); + Set> violations = validator.validate(new Repo("hg")); + assertThat(violations).isEmpty(); + } + + @Test + @DefaultLocale("en") + void shouldAddAvailableTypesToMessage() { + mockRepositoryTypes("git", "hg", "svn"); + + Validator validator = validator(); + Set> violations = validator.validate(new Repo("unknown")); + assertThat(violations).hasSize(1).allSatisfy( + violation -> assertThat(violation.getMessage()).contains("git, hg, svn") + ); + } + + private Validator validator() { + return Validation.buildDefaultValidatorFactory() + .usingContext() + .constraintValidatorFactory(new TestingConstraintValidatorFactory(repositoryManager)) + .getValidator(); + } + + private void mockRepositoryTypes(String... types) { + List repositoryTypes = Arrays.stream(types) + .map(t -> new RepositoryType(t, t.toUpperCase(Locale.ENGLISH), Collections.emptySet())) + .collect(Collectors.toList()); + + when(repositoryManager.getConfiguredTypes()).thenReturn(repositoryTypes); + } + + public static class Repo { + + @RepositoryTypeConstraint + private final String type; + + public Repo(String type) { + this.type = type; + } + } + + public static class TestingConstraintValidatorFactory implements ConstraintValidatorFactory { + + private final RepositoryManager repositoryManager; + + public TestingConstraintValidatorFactory(RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + } + + @Override + public > T getInstance(Class key) { + try { + return key.getConstructor(RepositoryManager.class).newInstance(repositoryManager); + } catch (Exception ex) { + try { + return key.getConstructor().newInstance(); + } catch (Exception e) { + throw new IllegalStateException("Failed to create constraint", e); + } + } + } + + @Override + public void releaseInstance(ConstraintValidator instance) { + + } + } + +} diff --git a/scm-core/src/test/resources/sonia/scm/cli/i18n.properties b/scm-core/src/test/resources/sonia/scm/cli/i18n.properties new file mode 100644 index 0000000000..17d38f5613 --- /dev/null +++ b/scm-core/src/test/resources/sonia/scm/cli/i18n.properties @@ -0,0 +1,25 @@ +# +# 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. +# + +errorLabel= ERROR diff --git a/scm-core/src/test/resources/sonia/scm/cli/i18n_de.properties b/scm-core/src/test/resources/sonia/scm/cli/i18n_de.properties new file mode 100644 index 0000000000..b4cc2fa4fd --- /dev/null +++ b/scm-core/src/test/resources/sonia/scm/cli/i18n_de.properties @@ -0,0 +1,25 @@ +# +# 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. +# + +errorLabel= FEHLER diff --git a/scm-dao-xml/gradle.lockfile b/scm-dao-xml/gradle.lockfile index 0dbf23980d..9a41417e25 100644 --- a/scm-dao-xml/gradle.lockfile +++ b/scm-dao-xml/gradle.lockfile @@ -29,6 +29,7 @@ commons-beanutils:commons-beanutils:1.9.4=compileClasspath,compileClasspathCopy, commons-collections:commons-collections:3.2.2=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy commons-lang:commons-lang:2.6=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy de.otto.edison:edison-hal:2.1.0=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +info.picocli:picocli:4.6.3=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.micrometer:micrometer-core:1.6.4=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.smallrye.common:smallrye-common-annotation:1.6.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.smallrye.common:smallrye-common-classloader:1.6.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy diff --git a/scm-it/gradle.lockfile b/scm-it/gradle.lockfile index 956eddbca6..f1e1de086b 100644 --- a/scm-it/gradle.lockfile +++ b/scm-it/gradle.lockfile @@ -73,6 +73,7 @@ commons-lang:commons-lang:2.6=testCompileClasspath,testCompileClasspathCopy,test commons-logging:commons-logging:1.2=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy de.otto.edison:edison-hal:2.1.0=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy de.regnis.q.sequence:sequence-library:1.0.4=testRuntimeClasspath,testRuntimeClasspathCopy +info.picocli:picocli:4.6.3=testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.github.mweirauch:micrometer-jvm-extras:0.2.2=testRuntimeClasspath,testRuntimeClasspathCopy io.jsonwebtoken:jjwt-api:0.11.2=testRuntimeClasspath,testRuntimeClasspathCopy io.jsonwebtoken:jjwt-impl:0.11.2=testRuntimeClasspath,testRuntimeClasspathCopy diff --git a/scm-plugins/scm-git-plugin/gradle.lockfile b/scm-plugins/scm-git-plugin/gradle.lockfile index 8705f772d4..e0f43d24d2 100644 --- a/scm-plugins/scm-git-plugin/gradle.lockfile +++ b/scm-plugins/scm-git-plugin/gradle.lockfile @@ -53,6 +53,7 @@ commons-collections:commons-collections:3.2.2=compileClasspath,default,runtimeCl commons-lang:commons-lang:2.6=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath commons-logging:commons-logging:1.2=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath de.otto.edison:edison-hal:2.1.0=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.6.3=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.github.classgraph:classgraph:4.8.65=swaggerDeps io.micrometer:micrometer-core:1.6.4=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.smallrye.common:smallrye-common-annotation:1.6.0=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath @@ -161,5 +162,5 @@ sonia.jgit:org.eclipse.jgit.junit:5.11.1.202105131744-r-scm1=testCompileClasspat sonia.jgit:org.eclipse.jgit.lfs.server:5.11.1.202105131744-r-scm1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath sonia.jgit:org.eclipse.jgit.lfs:5.11.1.202105131744-r-scm1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath sonia.jgit:org.eclipse.jgit:5.11.1.202105131744-r-scm1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -sonia.scm:scm-webapp:2.30.2-SNAPSHOT=scmServer +sonia.scm:scm-webapp:2.32.2-SNAPSHOT=scmServer empty=archives,optionalPlugin,plugin diff --git a/scm-plugins/scm-hg-plugin/gradle.lockfile b/scm-plugins/scm-hg-plugin/gradle.lockfile index 42f1aee78a..dbdd9530a0 100644 --- a/scm-plugins/scm-hg-plugin/gradle.lockfile +++ b/scm-plugins/scm-hg-plugin/gradle.lockfile @@ -51,6 +51,7 @@ commons-beanutils:commons-beanutils:1.9.4=compileClasspath,default,runtimeClassp commons-collections:commons-collections:3.2.2=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath commons-lang:commons-lang:2.6=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath de.otto.edison:edison-hal:2.1.0=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.6.3=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.github.classgraph:classgraph:4.8.65=swaggerDeps io.micrometer:micrometer-core:1.6.4=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.smallrye.common:smallrye-common-annotation:1.6.0=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath @@ -139,5 +140,5 @@ org.slf4j:jcl-over-slf4j:1.7.30=compileClasspath,default,runtimeClasspath,runtim org.slf4j:slf4j-api:1.7.25=swaggerDeps org.slf4j:slf4j-api:1.7.30=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath org.yaml:snakeyaml:1.26=swaggerDeps -sonia.scm:scm-webapp:2.30.2-SNAPSHOT=scmServer +sonia.scm:scm-webapp:2.32.2-SNAPSHOT=scmServer empty=archives,optionalPlugin,plugin diff --git a/scm-plugins/scm-integration-test-plugin/gradle.lockfile b/scm-plugins/scm-integration-test-plugin/gradle.lockfile index fc51a5ddd0..87297d491c 100644 --- a/scm-plugins/scm-integration-test-plugin/gradle.lockfile +++ b/scm-plugins/scm-integration-test-plugin/gradle.lockfile @@ -49,6 +49,7 @@ commons-beanutils:commons-beanutils:1.9.4=compileClasspath,default,runtimeClassp commons-collections:commons-collections:3.2.2=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath commons-lang:commons-lang:2.6=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath de.otto.edison:edison-hal:2.1.0=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.6.3=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.github.classgraph:classgraph:4.8.65=swaggerDeps io.micrometer:micrometer-core:1.6.4=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.smallrye.common:smallrye-common-annotation:1.6.0=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath @@ -136,5 +137,5 @@ org.slf4j:jcl-over-slf4j:1.7.30=compileClasspath,default,runtimeClasspath,runtim org.slf4j:slf4j-api:1.7.25=swaggerDeps org.slf4j:slf4j-api:1.7.30=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath org.yaml:snakeyaml:1.26=swaggerDeps -sonia.scm:scm-webapp:2.30.2-SNAPSHOT=scmServer +sonia.scm:scm-webapp:2.32.2-SNAPSHOT=scmServer empty=archives,optionalPlugin,plugin diff --git a/scm-plugins/scm-legacy-plugin/gradle.lockfile b/scm-plugins/scm-legacy-plugin/gradle.lockfile index fc51a5ddd0..87297d491c 100644 --- a/scm-plugins/scm-legacy-plugin/gradle.lockfile +++ b/scm-plugins/scm-legacy-plugin/gradle.lockfile @@ -49,6 +49,7 @@ commons-beanutils:commons-beanutils:1.9.4=compileClasspath,default,runtimeClassp commons-collections:commons-collections:3.2.2=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath commons-lang:commons-lang:2.6=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath de.otto.edison:edison-hal:2.1.0=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.6.3=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.github.classgraph:classgraph:4.8.65=swaggerDeps io.micrometer:micrometer-core:1.6.4=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.smallrye.common:smallrye-common-annotation:1.6.0=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath @@ -136,5 +137,5 @@ org.slf4j:jcl-over-slf4j:1.7.30=compileClasspath,default,runtimeClasspath,runtim org.slf4j:slf4j-api:1.7.25=swaggerDeps org.slf4j:slf4j-api:1.7.30=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath org.yaml:snakeyaml:1.26=swaggerDeps -sonia.scm:scm-webapp:2.30.2-SNAPSHOT=scmServer +sonia.scm:scm-webapp:2.32.2-SNAPSHOT=scmServer empty=archives,optionalPlugin,plugin diff --git a/scm-plugins/scm-svn-plugin/gradle.lockfile b/scm-plugins/scm-svn-plugin/gradle.lockfile index 5d80bcf208..e75e8f7eaf 100644 --- a/scm-plugins/scm-svn-plugin/gradle.lockfile +++ b/scm-plugins/scm-svn-plugin/gradle.lockfile @@ -58,6 +58,7 @@ commons-collections:commons-collections:3.2.2=compileClasspath,default,runtimeCl commons-lang:commons-lang:2.6=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath de.otto.edison:edison-hal:2.1.0=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath de.regnis.q.sequence:sequence-library:1.0.4=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.6.3=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.github.classgraph:classgraph:4.8.65=swaggerDeps io.micrometer:micrometer-core:1.6.4=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath io.smallrye.common:smallrye-common-annotation:1.6.0=compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath @@ -152,7 +153,7 @@ org.slf4j:slf4j-api:1.7.25=swaggerDeps org.slf4j:slf4j-api:1.7.30=annotationProcessor,compileClasspath,default,runtimeClasspath,runtimePluginElements,scmCoreDependency,testCompileClasspath,testRuntimeClasspath org.tmatesoft.sqljet:sqljet:1.1.14=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.yaml:snakeyaml:1.26=swaggerDeps -sonia.scm:scm-webapp:2.30.2-SNAPSHOT=scmServer +sonia.scm:scm-webapp:2.32.2-SNAPSHOT=scmServer sonia.svnkit:svnkit-dav:1.10.3-scm1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath sonia.svnkit:svnkit:1.10.3-scm1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath empty=archives,optionalPlugin,plugin diff --git a/scm-test/gradle.lockfile b/scm-test/gradle.lockfile index 326b4df4af..e3a423bc46 100644 --- a/scm-test/gradle.lockfile +++ b/scm-test/gradle.lockfile @@ -28,6 +28,7 @@ commons-beanutils:commons-beanutils:1.9.4=compileClasspath,compileClasspathCopy, commons-collections:commons-collections:3.2.2=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy commons-lang:commons-lang:2.6=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy de.otto.edison:edison-hal:2.1.0=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +info.picocli:picocli:4.6.3=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.micrometer:micrometer-core:1.6.4=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.smallrye.common:smallrye-common-annotation:1.6.0=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.smallrye.common:smallrye-common-classloader:1.6.0=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy diff --git a/scm-webapp/gradle.lockfile b/scm-webapp/gradle.lockfile index 8de3bda15c..1dcf85110d 100644 --- a/scm-webapp/gradle.lockfile +++ b/scm-webapp/gradle.lockfile @@ -62,6 +62,7 @@ commons-io:commons-io:2.9.0=compileClasspath,compileClasspathCopy,default,defaul commons-lang:commons-lang:2.6=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy commons-logging:commons-logging:1.2=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy de.otto.edison:edison-hal:2.1.0=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +info.picocli:picocli:4.6.3=annotationProcessor,annotationProcessorCopy,compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.github.classgraph:classgraph:4.8.117=compileClasspath,compileClasspathCopy,swaggerDeps,swaggerDepsCopy io.github.mweirauch:micrometer-jvm-extras:0.2.2=compileClasspath,compileClasspathCopy,default,defaultCopy,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.github.toolfactory:jvm-driver:4.0.0=compileClasspath,compileClasspathCopy,swaggerDeps,swaggerDepsCopy diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CliResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CliResource.java new file mode 100644 index 0000000000..38cd014b09 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CliResource.java @@ -0,0 +1,95 @@ +/* + * 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.api.v2.resources; + +import lombok.Data; +import org.apache.shiro.SecurityUtils; +import sonia.scm.cli.CliProcessor; +import sonia.scm.cli.JsonStreamingCliContext; +import sonia.scm.security.ApiKeyService; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.util.List; + +@Path("v2/cli") +public class CliResource { + + private final CliProcessor processor; + private final ApiKeyService service; + + @Inject + public CliResource(CliProcessor processor, ApiKeyService service) { + this.processor = processor; + this.service = service; + } + + @POST + @Path("exec") + public StreamingOutput exec(@QueryParam("args") List args, @Context HttpServletRequest request) { + return outputStream -> { + try (JsonStreamingCliContext context = new JsonStreamingCliContext(request.getLocale(), request.getInputStream(), outputStream)) { + int exitCode = processor.execute(context, args.toArray(new String[0])); + context.writeExit(exitCode); + } + }; + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("login") + public Response login(CliAuthenticationDto auth) { + String username = SecurityUtils.getSubject().getPrincipal().toString(); + ApiKeyService.CreationResult newKey = service.createNewKey(username, auth.getApiKey(), "*"); + return Response.ok(newKey.getToken()).build(); + } + + @DELETE + @Path("logout/{apiKey}") + public Response logout(@PathParam("apiKey") String apiKeyName) { + String username = SecurityUtils.getSubject().getPrincipal().toString(); + service.getKeys(username) + .stream() + .filter(apiKey -> apiKey.getDisplayName().equals(apiKeyName)) + .findFirst() + .ifPresent(apiKey -> service.remove(username, apiKey.getId())); + return Response.noContent().build(); + } + + @Data + static class CliAuthenticationDto { + private String apiKey; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CliExceptionHandler.java b/scm-webapp/src/main/java/sonia/scm/cli/CliExceptionHandler.java new file mode 100644 index 0000000000..c8afc03c34 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/CliExceptionHandler.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 picocli.CommandLine; +import sonia.scm.ExceptionWithContext; + +import java.io.PrintWriter; + +public class CliExceptionHandler implements CommandLine.IExecutionExceptionHandler { + + @Override + public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { + if (ex instanceof CliExitException) { + return ((CliExitException) ex).getExitCode(); + } + + PrintWriter stdErr = commandLine.getErr(); + stdErr.print("Execution error"); + if (ex instanceof ExceptionWithContext) { + String code = ((ExceptionWithContext) ex).getCode(); + stdErr.print(" " + code); + } + stdErr.print(": "); + stdErr.println(ex.getMessage()); + stdErr.println(); + commandLine.usage(stdErr); + return ExitCode.SERVER_ERROR; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CliExitException.java b/scm-webapp/src/main/java/sonia/scm/cli/CliExitException.java new file mode 100644 index 0000000000..550d8e7993 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/CliExitException.java @@ -0,0 +1,37 @@ +/* + * 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 lombok.Getter; + +@Getter +public class CliExitException extends RuntimeException { + + private final int exitCode; + + public CliExitException(int exitCode) { + this.exitCode = exitCode; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CliProcessor.java b/scm-webapp/src/main/java/sonia/scm/cli/CliProcessor.java new file mode 100644 index 0000000000..4807a02c2a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/CliProcessor.java @@ -0,0 +1,81 @@ +/* + * 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 com.google.inject.Injector; +import picocli.AutoComplete; +import picocli.CommandLine; + +import javax.inject.Inject; +import java.util.ResourceBundle; + +public class CliProcessor { + + private final CommandRegistry registry; + private final Injector injector; + private final CommandLine.Model.CommandSpec usageHelp; + + @Inject + public CliProcessor(CommandRegistry registry, Injector injector) { + this.registry = registry; + this.injector = injector; + this.usageHelp = new CommandLine(HelpMixin.class).getCommandSpec(); + } + + public int execute(CliContext context, String... args) { + CommandFactory factory = new CommandFactory(injector, context); + CommandLine cli = new CommandLine(ScmManagerCommand.class, factory); + cli.getCommandSpec().addMixin("help", usageHelp); + cli.setResourceBundle(ResourceBundle.getBundle("sonia.scm.cli.i18n", context.getLocale())); + for (RegisteredCommandNode c : registry.createCommandTree()) { + CommandLine commandline = createCommandline(context, factory, c); + cli.getCommandSpec().addSubcommand(c.getName(), commandline); + } + cli.addSubcommand(AutoComplete.GenerateCompletion.class); + cli.setErr(context.getStderr()); + cli.setOut(context.getStdout()); + cli.setExecutionExceptionHandler(new CliExceptionHandler()); + return cli.execute(args); + } + + private CommandLine createCommandline(CliContext context, CommandFactory factory, RegisteredCommandNode command) { + 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(ResourceBundle.getBundle(resourceBundleBaseName, context.getLocale())); + } + for (RegisteredCommandNode child : command.getChildren()) { + if (!commandLine.getCommandSpec().subcommands().containsKey(child.getName())) { + CommandLine childCommandLine = createCommandline(context, factory, child); + commandLine.getCommandSpec().addSubcommand(child.getName(), childCommandLine); + } + } + + return commandLine; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CommandFactory.java b/scm-webapp/src/main/java/sonia/scm/cli/CommandFactory.java new file mode 100644 index 0000000000..9c2eda306e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/CommandFactory.java @@ -0,0 +1,57 @@ +/* + * 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 com.google.inject.AbstractModule; +import com.google.inject.Injector; +import picocli.CommandLine; + +public class CommandFactory implements CommandLine.IFactory { + + private final Injector injector; + + public CommandFactory(Injector injector, CliContext context) { + this.injector = injector.createChildInjector(new CliContextModule(context)); + } + + @Override + public K create(Class cls) throws Exception { + return injector.getInstance(cls); + } + + static class CliContextModule extends AbstractModule { + + private final CliContext context; + + private CliContextModule(CliContext context) { + this.context = context; + } + + @Override + protected void configure() { + bind(CliContext.class).toInstance(context); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CommandRegistry.java b/scm-webapp/src/main/java/sonia/scm/cli/CommandRegistry.java new file mode 100644 index 0000000000..9eebc248ce --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/CommandRegistry.java @@ -0,0 +1,69 @@ +/* + * 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 javax.inject.Inject; +import javax.inject.Singleton; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@Singleton +public class CommandRegistry { + + private final RegisteredCommandCollector commandCollector; + + @Inject + public CommandRegistry(RegisteredCommandCollector commandCollector) { + this.commandCollector = commandCollector; + } + + public Set createCommandTree() { + Set rootCommands = new HashSet<>(); + Set registeredCommands = commandCollector.collect(); + + Map, RegisteredCommandNode> commandNodes = new HashMap<>(); + + for (RegisteredCommand command : registeredCommands) { + commandNodes.put(command.getCommand(), new RegisteredCommandNode(command.getName(), command.getCommand())); + } + + for (RegisteredCommand command : registeredCommands) { + RegisteredCommandNode node = commandNodes.get(command.getCommand()); + if (command.getParent() == null) { + rootCommands.add(node); + } else { + RegisteredCommandNode parentNode = commandNodes.get(command.getParent()); + if (parentNode != null) { + parentNode.getChildren().add(node); + } else { + throw new NonExistingParentCommandException("parent command of " + command.getName() + " does not exist"); + } + } + } + return rootCommands; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/HelpMixin.java b/scm-webapp/src/main/java/sonia/scm/cli/HelpMixin.java new file mode 100644 index 0000000000..01d444365e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/HelpMixin.java @@ -0,0 +1,33 @@ +/* + * 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 picocli.CommandLine; + +public class HelpMixin { + + @CommandLine.Option(names = {"--help", "-h"}, usageHelp = true, descriptionKey = "scm.help.usage.description.0") + private boolean usageHelp; +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/JsonStreamingCliContext.java b/scm-webapp/src/main/java/sonia/scm/cli/JsonStreamingCliContext.java new file mode 100644 index 0000000000..808fae3aef --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/JsonStreamingCliContext.java @@ -0,0 +1,158 @@ +/* + * 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 com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.util.MinimalPrettyPrinter; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.util.Arrays; +import java.util.Locale; + +public class JsonStreamingCliContext implements CliContext, AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(JsonStreamingCliContext.class); + + private final Locale locale; + private final InputStream stdin; + private final PrintWriter stdout; + private final PrintWriter stderr; + private final JsonGenerator jsonGenerator; + + @SuppressWarnings("java:S2095") // generator is closed in the close method + public JsonStreamingCliContext(Locale locale, InputStream stdin, OutputStream output) throws IOException { + this.locale = locale; + this.stdin = stdin; + this.jsonGenerator = mapper.createGenerator(output).setPrettyPrinter(new MinimalPrettyPrinter("")); + jsonGenerator.writeStartArray(); + + StringingOutputWriter out = new StringingOutputWriter(jsonGenerator, "out"); + StringingOutputWriter err = new StringingOutputWriter(jsonGenerator, "err"); + out.setOther(err); + err.setOther(out); + + this.stdout = new PrintWriter(out); + this.stderr = new PrintWriter(err); + } + + @Override + public PrintWriter getStdout() { + return stdout; + } + + @Override + public PrintWriter getStderr() { + return stderr; + } + + @Override + public InputStream getStdin() { + return stdin; + } + + public void writeExit(int exitcode) throws IOException { + stdout.flush(); + stderr.flush(); + jsonGenerator.writeStartObject(); + jsonGenerator.writeNumberField("exit", exitcode); + jsonGenerator.writeEndObject(); + } + + @Override + public void exit(int exitCode) { + throw new CliExitException(exitCode); + } + + @Override + public Locale getLocale() { + return locale; + } + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Override + public void close() { + try { + stdout.close(); + stderr.close(); + jsonGenerator.writeEndArray(); + jsonGenerator.close(); + } catch (IOException e) { + LOG.error("Could not close cli output streams", e); + } + try { + stdin.close(); + } catch (IOException e) { + LOG.error("Could not close cli input stream", e); + } + } + + public static class StringingOutputWriter extends Writer { + + private final JsonGenerator jsonGenerator; + private final String name; + + private StringBuilder buffer = new StringBuilder(); + private StringingOutputWriter other; + + private StringingOutputWriter(JsonGenerator jsonGenerator, String name) { + this.jsonGenerator = jsonGenerator; + this.name = name; + } + + public void setOther(StringingOutputWriter other) { + this.other = other; + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + other.flush(); + buffer.append(Arrays.copyOfRange(cbuf, off, len)); + } + + @Override + public void flush() throws IOException { + String content = buffer.toString(); + if (content.length() > 0) { + buffer = new StringBuilder(); + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField(name, content); + jsonGenerator.writeEndObject(); + } + } + + @Override + public void close() throws IOException { + this.flush(); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/LogoutCommand.java b/scm-webapp/src/main/java/sonia/scm/cli/LogoutCommand.java new file mode 100644 index 0000000000..8055dd0def --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/LogoutCommand.java @@ -0,0 +1,30 @@ +/* + * 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 picocli.CommandLine; + +@CommandLine.Command(name = "logout") +public class LogoutCommand {} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/NonExistingParentCommandException.java b/scm-webapp/src/main/java/sonia/scm/cli/NonExistingParentCommandException.java new file mode 100644 index 0000000000..735c50b08e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/NonExistingParentCommandException.java @@ -0,0 +1,35 @@ +/* + * 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; + +/** + * Exception is thrown if a command is registered with parent which does not exist. + * @since 2.33.0 + */ +public class NonExistingParentCommandException extends CliException { + public NonExistingParentCommandException(String message) { + super(message); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/PingCommand.java b/scm-webapp/src/main/java/sonia/scm/cli/PingCommand.java new file mode 100644 index 0000000000..0b09af21eb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/PingCommand.java @@ -0,0 +1,45 @@ +/* + * 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 picocli.CommandLine; + +import javax.inject.Inject; + +@CommandLine.Command(name = "ping", hidden = true) +public class PingCommand implements Runnable { + + private final CliContext context; + + @Inject + public PingCommand(CliContext context) { + this.context = context; + } + + @Override + public void run() { + context.getStdout().println("PONG"); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommand.java b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommand.java new file mode 100644 index 0000000000..5bd645f40b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommand.java @@ -0,0 +1,36 @@ +/* + * 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 lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +class RegisteredCommand { + private String name; + private Class command; + private Class parent; +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandCollector.java b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandCollector.java new file mode 100644 index 0000000000..acc19c9c53 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandCollector.java @@ -0,0 +1,84 @@ +/* + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.plugin.ScmModule; + +import javax.inject.Inject; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public class RegisteredCommandCollector { + + private static final Logger LOG = LoggerFactory.getLogger(RegisteredCommandCollector.class); + + private final PluginLoader pluginLoader; + + @Inject + public RegisteredCommandCollector(PluginLoader pluginLoader) { + this.pluginLoader = pluginLoader; + } + + public Set collect() { + Set cmds = new HashSet<>(); + findCommands(pluginLoader.getUberClassLoader(), cmds, pluginLoader.getInstalledModules()); + findCommands(pluginLoader.getUberClassLoader(), cmds, pluginLoader.getInstalledPlugins().stream().map(InstalledPlugin::getDescriptor).collect(Collectors.toList())); + return Collections.unmodifiableSet(cmds); + } + + private void findCommands(ClassLoader classLoader, Set commands, Iterable modules) { + modules.forEach(m -> m.getCliCommands().forEach(c -> { + Class command = createCommand(classLoader, c.getClazz()); + if (command != null && command != ScmManagerCommand.class) { + commands.add(new RegisteredCommand(c.getName(), command, getParent(command))); + } + })); + } + + + private Class getParent(Class command) { + ParentCommand parentAnnotation = command.getAnnotation(ParentCommand.class); + if (parentAnnotation != null) { + return parentAnnotation.value(); + } + return null; + } + + private Class createCommand(ClassLoader classLoader, String clazz) { + try { + return classLoader.loadClass(clazz); + } catch (ClassNotFoundException e) { + LOG.error("Could not find command class: {}", clazz, e); + return null; + } + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandNode.java b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandNode.java new file mode 100644 index 0000000000..264fa9c259 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandNode.java @@ -0,0 +1,42 @@ +/* + * 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 lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class RegisteredCommandNode { + private final String name; + private final Class command; + private final List children = new ArrayList<>(); + + public RegisteredCommandNode(String name, Class command) { + this.name = name; + this.command = command; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/ScmManagerCommand.java b/scm-webapp/src/main/java/sonia/scm/cli/ScmManagerCommand.java new file mode 100644 index 0000000000..d19b9c45eb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/ScmManagerCommand.java @@ -0,0 +1,30 @@ +/* + * 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 picocli.CommandLine; + +@CommandLine.Command(name = "scm") +public class ScmManagerCommand {} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java index 08781b0070..5a3c4868d0 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ApplicationModuleProvider.java @@ -37,6 +37,7 @@ import sonia.scm.filter.WebElementModule; import sonia.scm.plugin.ExtensionProcessor; import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.ExecutorModule; +import sonia.scm.validation.ValidationModule; import javax.servlet.ServletContext; import java.util.ArrayList; @@ -62,6 +63,7 @@ public class ApplicationModuleProvider implements ModuleProvider { private List createModules(ClassOverrides overrides) { List moduleList = new ArrayList<>(); + moduleList.add(new ValidationModule()); moduleList.add(new ResteasyModule()); moduleList.add(ShiroWebModule.guiceFilterModule()); moduleList.add(new WebElementModule(pluginLoader)); diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryCommandDto.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryCommandDto.java new file mode 100644 index 0000000000..e934fd6393 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryCommandDto.java @@ -0,0 +1,43 @@ +/* + * 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.repository.cli; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class RepositoryCommandDto { + private String name; + private String namespace; + private String type; + private String contact; + private String description; + private String creationDate; + private String lastModified; + private String url; +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryCreateCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryCreateCommand.java new file mode 100644 index 0000000000..6dc8054fd7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryCreateCommand.java @@ -0,0 +1,116 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import picocli.CommandLine; +import sonia.scm.cli.CommandValidator; +import sonia.scm.cli.ParentCommand; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryInitializer; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryName; +import sonia.scm.repository.RepositoryTypeConstraint; + +import javax.inject.Inject; +import javax.validation.constraints.Email; + +@CommandLine.Command(name = "create") +@ParentCommand(value = RepositoryCommand.class) +public class RepositoryCreateCommand implements Runnable { + + @CommandLine.Mixin + private final RepositoryTemplateRenderer templateRenderer; + @CommandLine.Mixin + private final CommandValidator validator; + private final RepositoryManager manager; + private final RepositoryInitializer repositoryInitializer; + + @RepositoryTypeConstraint + @CommandLine.Parameters(descriptionKey = "scm.repo.create.type") + private String type; + + @RepositoryName(namespace = RepositoryName.Namespace.OPTIONAL) + @CommandLine.Parameters(descriptionKey = "scm.repo.create.repository", paramLabel = "name") + private String repository; + + @CommandLine.Option(names = {"--description", "-d"}, descriptionKey = "scm.repo.create.desc") + private String description; + + @Email + @CommandLine.Option(names = {"--contact", "-c"}) + private String contact; + + @CommandLine.Option(names = {"--init", "-i"}, descriptionKey = "scm.repo.create.init") + private boolean init; + + @Inject + public RepositoryCreateCommand(RepositoryTemplateRenderer templateRenderer, + CommandValidator validator, + RepositoryManager manager, + RepositoryInitializer repositoryInitializer) { + this.templateRenderer = templateRenderer; + this.validator = validator; + this.manager = manager; + this.repositoryInitializer = repositoryInitializer; + } + + @Override + public void run() { + validator.validate(); + Repository newRepo = new Repository(); + String[] splitRepoName = repository.split("/"); + if (splitRepoName.length == 2) { + newRepo.setNamespace(splitRepoName[0]); + newRepo.setName(splitRepoName[1]); + } else { + newRepo.setName(repository); + } + newRepo.setType(type); + newRepo.setDescription(description); + newRepo.setContact(contact); + Repository createdRepo = manager.create(newRepo); + if (init) { + repositoryInitializer.initialize(createdRepo, ImmutableMap.of()); + } + templateRenderer.render(createdRepo); + } + + @VisibleForTesting + void setType(String type) { + this.type = type; + } + + @VisibleForTesting + void setRepository(String repository) { + this.repository = repository; + } + + @VisibleForTesting + void setInit(boolean init) { + this.init = init; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryDeleteCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryDeleteCommand.java new file mode 100644 index 0000000000..80130f46e5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryDeleteCommand.java @@ -0,0 +1,85 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Inject; +import java.util.Collections; + +@CommandLine.Command(name = "delete", aliases = "rm") +@ParentCommand(RepositoryCommand.class) +public class RepositoryDeleteCommand implements Runnable { + + private static final String PROMPT_TEMPLATE = "{{i18n.repoDeletePrompt}}"; + + @CommandLine.Parameters(descriptionKey = "scm.repo.delete.repository", paramLabel = "namespace/name") + private String repository; + + @CommandLine.Option(names = {"--yes", "-y"}, descriptionKey = "scm.repo.delete.prompt") + private boolean shouldDelete; + + @CommandLine.Mixin + private final RepositoryTemplateRenderer templateRenderer; + private final RepositoryManager manager; + + @Inject + public RepositoryDeleteCommand(RepositoryManager manager, RepositoryTemplateRenderer templateRenderer) { + this.manager = manager; + this.templateRenderer = templateRenderer; + } + + @Override + public void run() { + if (!shouldDelete) { + templateRenderer.renderToStderr(PROMPT_TEMPLATE, Collections.emptyMap()); + return; + } + String[] splitRepo = repository.split("/"); + if (splitRepo.length == 2) { + Repository repo = manager.get(new NamespaceAndName(splitRepo[0], splitRepo[1])); + if (repo != null) { + manager.delete(repo); + } + } else { + templateRenderer.renderInvalidInputError(); + } + } + + @VisibleForTesting + void setRepository(String repository) { + this.repository = repository; + } + + @VisibleForTesting + void setShouldDelete(boolean shouldDelete) { + this.shouldDelete = shouldDelete; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryGetCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryGetCommand.java new file mode 100644 index 0000000000..e0057dbac9 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryGetCommand.java @@ -0,0 +1,72 @@ +/* + * 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.repository.cli; + +import com.cronutils.utils.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Inject; + +@ParentCommand(value = RepositoryCommand.class) +@CommandLine.Command(name = "get") +public class RepositoryGetCommand implements Runnable { + + @CommandLine.Parameters(paramLabel = "namespace/name", index = "0") + private String repository; + + @CommandLine.Mixin + private final RepositoryTemplateRenderer templateRenderer; + private final RepositoryManager manager; + + @Inject + RepositoryGetCommand(RepositoryTemplateRenderer templateRenderer, RepositoryManager manager) { + this.templateRenderer = templateRenderer; + this.manager = manager; + } + + @VisibleForTesting + public void setRepository(String repository) { + this.repository = repository; + } + + @Override + public void run() { + String[] splitRepo = repository.split("/"); + if (splitRepo.length == 2) { + Repository repo = manager.get(new NamespaceAndName(splitRepo[0], splitRepo[1])); + if (repo != null) { + templateRenderer.render(repo); + } else { + templateRenderer.renderNotFoundError(); + } + } else { + templateRenderer.renderInvalidInputError(); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryListCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryListCommand.java new file mode 100644 index 0000000000..ae61c25a72 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryListCommand.java @@ -0,0 +1,89 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.cli.Table; +import sonia.scm.cli.TemplateRenderer; +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.stream.Collectors; + +@ParentCommand(value = RepositoryCommand.class) +@CommandLine.Command(name = "list", aliases = "ls") +public class RepositoryListCommand implements Runnable { + + @CommandLine.Mixin + private final TemplateRenderer templateRenderer; + private final RepositoryManager manager; + private final RepositoryToRepositoryCommandDtoMapper mapper; + + @CommandLine.Option(names = {"--short", "-s"}) + private boolean useShortTemplate; + + private static final String TABLE_TEMPLATE = String.join("\n", + "{{#rows}}", + "{{#cols}}{{#row.first}}{{#upper}}{{value}}{{/upper}}{{/row.first}}{{^row.first}}{{value}}{{/row.first}}{{^last}} {{/last}}{{/cols}}", + "{{/rows}}" + ); + + private static final String SHORT_TEMPLATE = String.join("\n", + "{{#repos}}", + "{{namespace}}/{{name}}", + "{{/repos}}" + ); + + @Inject + public RepositoryListCommand(RepositoryManager manager, TemplateRenderer templateRenderer, RepositoryToRepositoryCommandDtoMapper mapper) { + this.manager = manager; + this.templateRenderer = templateRenderer; + this.mapper = mapper; + } + + @Override + public void run() { + Collection dtos = manager.getAll().stream().map(mapper::map).collect(Collectors.toList()); + if (useShortTemplate) { + templateRenderer.renderToStdout(SHORT_TEMPLATE, ImmutableMap.of("repos", dtos)); + } else { + Table table = templateRenderer.createTable(); + table.addHeader("repoName", "repoType", "repoUrl"); + for (RepositoryCommandDto dto : dtos) { + table.addRow(dto.getNamespace() + "/" + dto.getName(), dto.getType(), dto.getUrl()); + } + templateRenderer.renderToStdout(TABLE_TEMPLATE, ImmutableMap.of("rows", table, "repos", dtos)); + } + } + + @VisibleForTesting + void setShortTemplate(boolean useShortTemplate) { + this.useShortTemplate = useShortTemplate; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryModifyCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryModifyCommand.java new file mode 100644 index 0000000000..6aba9c70e0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryModifyCommand.java @@ -0,0 +1,95 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.CommandValidator; +import sonia.scm.cli.ParentCommand; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Inject; +import javax.validation.constraints.Email; + +@ParentCommand(value = RepositoryCommand.class) +@CommandLine.Command(name = "modify") +public class RepositoryModifyCommand implements Runnable { + + @CommandLine.Mixin + private final RepositoryTemplateRenderer templateRenderer; + @CommandLine.Mixin + private final CommandValidator validator; + private final RepositoryManager manager; + + @CommandLine.Parameters(paramLabel = "namespace/name", index = "0", descriptionKey = "scm.repo.modify.repository") + private String repository; + + @CommandLine.Option(names = {"--description", "-d"}, descriptionKey = "scm.repo.create.desc") + private String description; + + @Email + @CommandLine.Option(names = {"--contact", "-c"}) + private String contact; + + + @Inject + RepositoryModifyCommand(RepositoryTemplateRenderer templateRenderer, CommandValidator validator, RepositoryManager manager) { + this.templateRenderer = templateRenderer; + this.validator = validator; + this.manager = manager; + } + + @VisibleForTesting + public void setRepository(String repository) { + this.repository = repository; + } + + @Override + public void run() { + validator.validate(); + String[] splitRepo = repository.split("/"); + if (splitRepo.length == 2) { + Repository repo = manager.get(new NamespaceAndName(splitRepo[0], splitRepo[1])); + + if (repo != null) { + if (contact != null) { + repo.setContact(contact); + } + if (description != null) { + repo.setDescription(description); + } + + manager.modify(repo); + templateRenderer.render(repo); + } else { + templateRenderer.renderNotFoundError(); + } + } else { + templateRenderer.renderInvalidInputError(); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryTemplateRenderer.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryTemplateRenderer.java new file mode 100644 index 0000000000..cc5676434f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryTemplateRenderer.java @@ -0,0 +1,86 @@ +/* + * 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.repository.cli; + +import com.google.common.collect.ImmutableMap; +import sonia.scm.cli.CliContext; +import sonia.scm.cli.ExitCode; +import sonia.scm.cli.Table; +import sonia.scm.cli.TemplateRenderer; +import sonia.scm.repository.Repository; +import sonia.scm.template.TemplateEngineFactory; + +import javax.inject.Inject; +import java.util.Collections; + +public class RepositoryTemplateRenderer extends TemplateRenderer { + + private static final String DETAILS_TABLE_TEMPLATE = String.join("\n", + "{{#rows}}", + "{{#cols}}{{value}}{{/cols}}", + "{{/rows}}" + ); + private static final String INVALID_INPUT_TEMPLATE = "{{i18n.repoInvalidInput}}"; + private static final String NOT_FOUND_TEMPLATE = "{{i18n.repoNotFound}}"; + + private final CliContext context; + private final RepositoryToRepositoryCommandDtoMapper mapper; + + @Inject + RepositoryTemplateRenderer(CliContext context, TemplateEngineFactory templateEngineFactory, RepositoryToRepositoryCommandDtoMapper mapper) { + super(context, templateEngineFactory); + this.context = context; + this.mapper = mapper; + } + + public void render(Repository repository) { + Table table = createTable(); + RepositoryCommandDto dto = mapper.map(repository); + table.addLabelValueRow("repoNamespace", dto.getNamespace()); + table.addLabelValueRow("repoName", dto.getName()); + table.addLabelValueRow("repoType", dto.getType()); + table.addLabelValueRow("repoContact", dto.getContact()); + table.addLabelValueRow("repoCreationDate", dto.getCreationDate()); + table.addLabelValueRow("repoLastModified", dto.getLastModified()); + table.addLabelValueRow("repoUrl", dto.getUrl()); + table.addLabelValueRow("repoDescription", dto.getDescription()); + renderToStdout(DETAILS_TABLE_TEMPLATE, ImmutableMap.of("rows", table, "repo", dto)); + } + + public void renderInvalidInputError() { + renderToStderr(INVALID_INPUT_TEMPLATE, Collections.emptyMap()); + context.exit(ExitCode.USAGE); + } + + public void renderNotFoundError() { + renderToStderr(NOT_FOUND_TEMPLATE, Collections.emptyMap()); + context.exit(ExitCode.NOT_FOUND); + } + + public void renderException(Exception exception) { + renderDefaultError(exception); + context.exit(ExitCode.SERVER_ERROR); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapper.java new file mode 100644 index 0000000000..67230e5814 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapper.java @@ -0,0 +1,69 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import org.mapstruct.Mapper; +import org.mapstruct.ObjectFactory; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.api.ScmProtocol; + +import javax.inject.Inject; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +@Mapper +public abstract class RepositoryToRepositoryCommandDtoMapper { + + @Inject + private RepositoryServiceFactory serviceFactory; + + public abstract RepositoryCommandDto map(Repository modelObject); + + @ObjectFactory + RepositoryCommandDto createDto(Repository repository) { + RepositoryCommandDto dto = new RepositoryCommandDto(); + try (RepositoryService service = serviceFactory.create(repository)) { + Optional protocolUrl = service.getSupportedProtocols().filter(p -> p.getType().equals("http")).findFirst(); + protocolUrl.ifPresent(scmProtocol -> dto.setUrl(scmProtocol.getUrl())); + } + return dto; + } + + String mapTimestampToISODate(Long timestamp) { + if (timestamp != null) { + return DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(timestamp)); + } + return null; + } + + @VisibleForTesting + void setServiceFactory(RepositoryServiceFactory serviceFactory) { + this.serviceFactory = serviceFactory; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/ApiKeyRealm.java b/scm-webapp/src/main/java/sonia/scm/security/ApiKeyRealm.java index cb627a56e7..e1ece3788c 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/ApiKeyRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/ApiKeyRealm.java @@ -90,14 +90,18 @@ public class ApiKeyRealm extends AuthenticatingRealm { } private AuthenticationInfo buildAuthenticationInfo(AuthenticationToken token, ApiKeyService.CheckResult check) { - RepositoryRole repositoryRole = determineRole(check); - Scope scope = createScope(repositoryRole); - LOG.debug("login for user {} with api key limited to role {}", check.getUser(), check.getPermissionRole()); - return helper + DAORealmHelper.AuthenticationInfoBuilder builder = helper .authenticationInfoBuilder(check.getUser()) - .withSessionId(getPrincipal(token)) - .withScope(scope) - .build(); + .withSessionId(getPrincipal(token)); + + if (!check.getPermissionRole().equals("*")) { + RepositoryRole repositoryRole = determineRole(check); + Scope scope = createScope(repositoryRole); + LOG.debug("login for user {} with api key limited to role {}", check.getUser(), check.getPermissionRole()); + builder = builder.withScope(scope); + } + + return builder.build(); } private String getPassword(AuthenticationToken token) { diff --git a/scm-webapp/src/main/java/sonia/scm/validation/DefaultValidatorProvider.java b/scm-webapp/src/main/java/sonia/scm/validation/DefaultValidatorProvider.java new file mode 100644 index 0000000000..bc910c50a3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/validation/DefaultValidatorProvider.java @@ -0,0 +1,49 @@ +/* + * 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.validation; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.Validation; +import javax.validation.Validator; + +public class DefaultValidatorProvider implements Provider { + + private final ConstraintValidatorFactory constraintValidatorFactory; + + @Inject + public DefaultValidatorProvider(ConstraintValidatorFactory constraintValidatorFactory) { + this.constraintValidatorFactory = constraintValidatorFactory; + } + + @Override + public Validator get() { + return Validation.buildDefaultValidatorFactory() + .usingContext() + .constraintValidatorFactory(constraintValidatorFactory) + .getValidator(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/validation/GuiceConstraintValidatorFactory.java b/scm-webapp/src/main/java/sonia/scm/validation/GuiceConstraintValidatorFactory.java new file mode 100644 index 0000000000..304e8ad72e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/validation/GuiceConstraintValidatorFactory.java @@ -0,0 +1,51 @@ +/* + * 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.validation; + +import com.google.inject.Injector; + +import javax.inject.Inject; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorFactory; + +public class GuiceConstraintValidatorFactory implements ConstraintValidatorFactory { + + private final Injector injector; + + @Inject + public GuiceConstraintValidatorFactory(Injector injector) { + this.injector = injector; + } + + @Override + public > T getInstance(Class key) { + return injector.getInstance(key); + } + + @Override + public void releaseInstance(ConstraintValidator instance) { + // do nothing + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/validation/ResteasyValidator.java b/scm-webapp/src/main/java/sonia/scm/validation/ResteasyValidator.java new file mode 100644 index 0000000000..2ae8e4de12 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/validation/ResteasyValidator.java @@ -0,0 +1,78 @@ +/* + * 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.validation; + +import org.jboss.resteasy.plugins.validation.GeneralValidatorImpl; +import org.jboss.resteasy.spi.HttpRequest; + +import javax.validation.ConstraintValidatorFactory; +import javax.validation.MessageInterpolator; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import javax.validation.executable.ExecutableType; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class ResteasyValidator extends GeneralValidatorImpl { + + private final ValidatorFactory validatorFactory; + private final ConstraintValidatorFactory constraintValidatorFactory; + + ResteasyValidator(ValidatorFactory validatorFactory, ConstraintValidatorFactory constraintValidatorFactory, boolean isExecutableValidationEnabled, Set defaultValidatedExecutableTypes) { + super(Validation.buildDefaultValidatorFactory(), isExecutableValidationEnabled, defaultValidatedExecutableTypes); + this.validatorFactory = validatorFactory; + this.constraintValidatorFactory = constraintValidatorFactory; + } + + @Override + protected Validator getValidator(HttpRequest request) { + Validator v = Validator.class.cast(request.getAttribute(Validator.class.getName())); + if (v == null) { + Locale locale = getLocaleFrom(request); + v = createValidator(locale); + request.setAttribute(Validator.class.getName(), v); + } + return v; + } + + private Validator createValidator(Locale locale) { + MessageInterpolator interpolator = new LocaleSpecificMessageInterpolator(validatorFactory.getMessageInterpolator(), locale); + return validatorFactory.usingContext() + .constraintValidatorFactory(constraintValidatorFactory) + .messageInterpolator(interpolator) + .getValidator(); + } + + private Locale getLocaleFrom(HttpRequest request) { + if (request == null) { + return null; + } + List locales = request.getHttpHeaders().getAcceptableLanguages(); + return locales == null || locales.isEmpty() ? Locale.ENGLISH : locales.get(0); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/validation/ResteasyValidatorContextResolver.java b/scm-webapp/src/main/java/sonia/scm/validation/ResteasyValidatorContextResolver.java new file mode 100644 index 0000000000..fe3c812983 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/validation/ResteasyValidatorContextResolver.java @@ -0,0 +1,59 @@ +/* + * 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.validation; + +import org.jboss.resteasy.spi.validation.GeneralValidator; + +import javax.inject.Inject; +import javax.validation.BootstrapConfiguration; +import javax.validation.Configuration; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.Validation; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +@Provider +public class ResteasyValidatorContextResolver implements ContextResolver { + + private final ConstraintValidatorFactory constraintValidatorFactory; + + @Inject + public ResteasyValidatorContextResolver(ConstraintValidatorFactory constraintValidatorFactory) { + this.constraintValidatorFactory = constraintValidatorFactory; + } + + @Override + public GeneralValidator getContext(Class type) { + Configuration configuration = Validation.byDefaultProvider().configure(); + BootstrapConfiguration bootstrapConfiguration = configuration.getBootstrapConfiguration(); + + return new ResteasyValidator( + configuration.buildValidatorFactory(), + constraintValidatorFactory, + bootstrapConfiguration.isExecutableValidationEnabled(), + bootstrapConfiguration.getDefaultValidatedExecutableTypes() + ); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/validation/ValidationModule.java b/scm-webapp/src/main/java/sonia/scm/validation/ValidationModule.java new file mode 100644 index 0000000000..99c9034e22 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/validation/ValidationModule.java @@ -0,0 +1,39 @@ +/* + * 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.validation; + +import com.google.inject.AbstractModule; + +import javax.validation.ConstraintValidatorFactory; +import javax.validation.Validator; + +public class ValidationModule extends AbstractModule { + + @Override + protected void configure() { + bind(ConstraintValidatorFactory.class).to(GuiceConstraintValidatorFactory.class); + bind(Validator.class).toProvider(DefaultValidatorProvider.class); + } +} diff --git a/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties b/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties new file mode 100644 index 0000000000..35b83aaf33 --- /dev/null +++ b/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties @@ -0,0 +1,81 @@ +# +# 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. +# + +errorCommandFailed= ____________Command failed____________ +errorUnknownError= Unknown error occurred. Check your server logs +errorLabel= ERROR + +repoNamespace = Namespace +repoName = Name +repoDescription= Description +repoContact= Contact +repoType= Type +repoUrl = Url +repoCreationDate = Creation Date +repoLastModified = Last Modified + +# picocli translations +usage.descriptionHeading = Description:\u0020 +usage.synopsisHeading = Usage:\u0020 + +## Template renderer +scm.templateRenderer.template = Specify rendering template + +## Help +scm.help.usage.description.0 = Show this help message and exit. + +## Logout +scm.logout.usage.description.0 = Removes the api key from server and local cli config + +## Ping +scm.ping.usage.description.0 = Returns PONG if the server is available + +## Repo +scm.repo.usage.description.0 = Parent command for all repository-related actions +repoNotFound= Could not find repository +repoInvalidInput= Invalid input. Use namespace/name + +### Get repo +scm.repo.get.usage.description.0 = Returns repository related information + +### List repo +scm.repo.list.usage.description.0 = List all repositories on server + +### Create repo +scm.repo.create.usage.description.0 = Creates new repository on server +scm.repo.create.type = Repository type +scm.repo.create.repository = Repository name (namespace/name for custom namespace strategy) +scm.repo.create.contact = Repository contact mail address +scm.repo.create.desc = Repository description +scm.repo.create.init = Initialize repository + +### Delete repo +scm.repo.delete.usage.description.0 = Deletes repository on server +scm.repo.delete.repository = Repository namespace/name +scm.repo.delete.prompt = Set this flag to agree to delete the repository +repoDeletePrompt= If you really want to delete this repository please pass --yes + +### Modify repo +scm.repo.modify.usage.description.0 = Modify repository on server +scm.repo.modify.repository = Repository namespace/name diff --git a/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties b/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties new file mode 100644 index 0000000000..247aa55540 --- /dev/null +++ b/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties @@ -0,0 +1,81 @@ +# +# 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. +# + +errorCommandFailed= ____________Befehl fehlgeschlagen____________ +errorUnknownError= Unbekannter Fehler. Prüfen Sie Ihre Server Logs +errorLabel= FEHLER + +repoNamespace = Namespace +repoName = Name +repoDescription= Beschreibung +repoContact= Kontakt +repoType= Typ +repoUrl = Url +repoCreationDate = Erstellt +repoLastModified = Zuletzt bearbeitet + +# picocli translations +usage.descriptionHeading = Beschreibung:\u0020 +usage.synopsisHeading = Nutzung:\u0020 + +## Template renderer +scm.templateRenderer.template = Vorlage des Ausgabeformats spezifizieren + +## Help +scm.help.usage.description.0 = Zeigt die Hilfe an und stoppt die Ausführung. + +## Logout +scm.logout.usage.description.0 = Entfernt den API Key vom Server und die lokale CLI Konfiguration + +## Ping +scm.ping.usage.description.0 = Antwortet PONG, wenn der Server erreichbar ist + +## Repo +scm.repo.usage.description.0 = Gruppen Befehl für alle Repository Befehle +repoNotFound= Repository konnte nicht gefunden werden +repoInvalidInput= Ungültige Eingabe. Nutzen Sie namespace/name + +### Get repo +scm.repo.get.usage.description.0 = Liefert Informationen zum Repository + +### List repo +scm.repo.list.usage.description.0 = Listet alle Repositories + +### Create repo +scm.repo.create.usage.description.0 = Erzeugt ein neues Repository auf dem Server +scm.repo.create.type = Repository Typ +scm.repo.create.repository = Repository Name (Namespace/Name bei benutzerdefinierter Namespace Strategie) +scm.repo.create.contact = Repository Kontakt E-Mail Adresse +scm.repo.create.desc = Repository Beschreibung +scm.repo.create.init = Repository initialisieren + +### Delete repo +scm.repo.delete.usage.description.0 = Löscht ein Repository auf dem Server +scm.repo.delete.repository = Repository Namespace/Name +scm.repo.delete.prompt = Setzen Sie diese Option, um ein Repository endgültig zu löschen +repoDeletePrompt= Wenn dieses Repository endgültig gelöscht werden soll, setzen Sie bitte --yes + +### Modify repo +scm.repo.modify.usage.description.0 = Aktualisiert ein Repository auf dem Server +scm.repo.modify.repository = Repository Namespace/Name diff --git a/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java b/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java new file mode 100644 index 0000000000..8bf7d749c5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java @@ -0,0 +1,171 @@ +/* + * 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 com.google.inject.Guice; +import com.google.inject.Injector; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import picocli.CommandLine; + +import javax.annotation.Nonnull; +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CliProcessorTest { + + @Mock + private CommandRegistry registry; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private CliContext context; + + + @BeforeEach + void setDefaultLocale() { + when(context.getLocale()).thenReturn(Locale.ENGLISH); + } + + @Test + void shouldExecutePingCommand() { + when(registry.createCommandTree()).thenReturn(Collections.singleton(new RegisteredCommandNode("ping", PingCommand.class))); + Injector injector = Guice.createInjector(); + CliProcessor cliProcessor = new CliProcessor(registry, injector); + + cliProcessor.execute(context, "ping"); + + verify(context.getStdout()).println("PONG"); + } + + @Test + void shouldExecutePingCommandWithExitCode0() { + when(registry.createCommandTree()).thenReturn(Collections.singleton(new RegisteredCommandNode("ping", PingCommand.class))); + Injector injector = Guice.createInjector(); + CliProcessor cliProcessor = new CliProcessor(registry, injector); + + int exitCode = cliProcessor.execute(context, "ping"); + + assertThat(exitCode).isZero(); + } + + @Test + void shouldPrintCommandOne() { + String result = executeHierachyCommands("--help"); + + assertThat(result).contains("Commands:\n" + + " one"); + } + + @Test + void shouldPrintCommandTwo() { + String result = executeHierachyCommands("one","--help"); + + assertThat(result).contains("Commands:\n" + + " two"); + } + + @Test + void shouldPrintCommandThree() { + String result = executeHierachyCommands("one", "two","--help"); + + assertThat(result).contains("Commands:\n" + + " three"); + } + + @Nonnull + private String executeHierachyCommands(String... args) { + RegisteredCommandNode one = new RegisteredCommandNode("one", RootCommand.class); + RegisteredCommandNode two = new RegisteredCommandNode("two", SubCommand.class); + RegisteredCommandNode three = new RegisteredCommandNode("three", SubSubCommand.class); + two.getChildren().add(three); + one.getChildren().add(two); + + when(registry.createCommandTree()).thenReturn(Collections.singleton(one)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + when(context.getStdout()).thenReturn(new PrintWriter(baos)); + + Injector injector = Guice.createInjector(); + CliProcessor cliProcessor = new CliProcessor(registry, injector); + + cliProcessor.execute(context, args); + return baos.toString(); + } + + + @Test + void shouldUseResourceBundleFromAnnotationWithContextLocale() { + when(context.getLocale()).thenReturn(Locale.GERMAN); + + String helpForThree = executeHierachyCommands("one", "two", "three", "--help"); + + assertThat(helpForThree).contains("Dies ist meine App."); + } + + @Test + void shouldUseDefaultWithoutResourceBundle() { + when(context.getLocale()).thenReturn(Locale.GERMAN); + + String helpForTwo = executeHierachyCommands("one", "two", "--help"); + + assertThat(helpForTwo).contains("Dies ist meine App."); + } + + @CommandLine.Command(name = "one") + static class RootCommand implements Runnable { + + @Override + public void run() { + + } + } + + @CommandLine.Command(name = "two") + static class SubCommand implements Runnable { + + @Override + public void run() { + + } + } + + @CommandLine.Command(name = "three", resourceBundle = "sonia.scm.cli.test") + static class SubSubCommand implements Runnable { + @Override + public void run() { + + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/cli/CommandRegistryTest.java b/scm-webapp/src/test/java/sonia/scm/cli/CommandRegistryTest.java new file mode 100644 index 0000000000..813b36b5c6 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/cli/CommandRegistryTest.java @@ -0,0 +1,96 @@ +/* + * 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 com.google.common.collect.ImmutableSet; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + + +@ExtendWith(MockitoExtension.class) +class CommandRegistryTest { + + @Mock + private RegisteredCommandCollector commandCollector; + @InjectMocks + private CommandRegistry registry; + + @Test + void shouldCreateTreeWithOnlyRootNodes() { + mockCommands(rc(Object.class), rc(String.class), rc(Integer.class)); + + Set commandTree = registry.createCommandTree(); + assertContainsCommands(commandTree, Object.class, String.class, Integer.class); + } + + @Test + void shouldCreateTreeWithParents() { + mockCommands(rc(Object.class), rc(String.class, Object.class), rc(Integer.class, Object.class)); + + Set commandTree = registry.createCommandTree(); + + assertContainsCommands(commandTree, Object.class); + assertContainsCommands(commandTree.iterator().next().getChildren(), Integer.class, String.class); + } + + @Test + void shouldCreateTreeWithParentsSecondLevel() { + mockCommands(rc(Object.class), rc(String.class, Object.class), rc(Integer.class, String.class)); + + Set commandTree = registry.createCommandTree(); + + assertContainsCommands(commandTree, Object.class); + RegisteredCommandNode rootNode = commandTree.iterator().next(); + assertContainsCommands(rootNode.getChildren(), String.class); + assertContainsCommands(rootNode.getChildren().get(0).getChildren(), Integer.class); + } + + private void mockCommands(RegisteredCommand... commands) { + when(commandCollector.collect()).thenReturn(ImmutableSet.copyOf(commands)); + } + + private RegisteredCommand rc(Class command) { + return rc(command, null); + } + + private RegisteredCommand rc(Class command, Class parent) { + return new RegisteredCommand(command.getSimpleName(), command, parent); + } + + private void assertContainsCommands(Collection nodes, Class... expected) { + assertThat(nodes).map(RegisteredCommandNode::getCommand).contains(expected); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/cli/JsonStreamingCliContextTest.java b/scm-webapp/src/test/java/sonia/scm/cli/JsonStreamingCliContextTest.java new file mode 100644 index 0000000000..5abf3000c8 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/cli/JsonStreamingCliContextTest.java @@ -0,0 +1,89 @@ +/* + * 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 com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.output.ByteArrayOutputStream; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; + +class JsonStreamingCliContextTest { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Test + void shouldPrintJsonOnStdout() throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try (JsonStreamingCliContext jsonStreamingCliContext = new JsonStreamingCliContext(Locale.ENGLISH, bais, baos)) { + jsonStreamingCliContext.getStdout().print("Hello"); + } + + JsonNode json = mapper.readTree(new ByteArrayInputStream(baos.toByteArray())); + assertThat(json.isArray()).isTrue(); + assertThat(json.get(0).get("out").asText()).isEqualTo("Hello"); + } + + @Test + void shouldPrintJsonOnStdoutAndStderr() throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try (JsonStreamingCliContext jsonStreamingCliContext = new JsonStreamingCliContext(Locale.ENGLISH, bais, baos)) { + jsonStreamingCliContext.getStdout().print("Hello"); + jsonStreamingCliContext.getStderr().print("Error 1: Failed"); + jsonStreamingCliContext.getStdout().print(" World"); + } + + JsonNode json = mapper.readTree(new ByteArrayInputStream(baos.toByteArray())); + assertThat(json.isArray()).isTrue(); + assertThat(json.get(0).get("out").asText()).isEqualTo("Hello"); + assertThat(json.get(1).get("err").asText()).isEqualTo("Error 1: Failed"); + assertThat(json.get(2).get("out").asText()).isEqualTo(" World"); + } + + @Test + void shouldReturnExitCode() throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try (JsonStreamingCliContext jsonStreamingCliContext = new JsonStreamingCliContext(Locale.ENGLISH, bais, baos)) { + jsonStreamingCliContext.getStdout().print("Hello"); + jsonStreamingCliContext.writeExit(1); + } + + JsonNode json = mapper.readTree(new ByteArrayInputStream(baos.toByteArray())); + assertThat(json.isArray()).isTrue(); + assertThat(json.get(0).get("out").asText()).isEqualTo("Hello"); + assertThat(json.get(1).get("exit").asInt()).isEqualTo(1); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/cli/RegisteredCommandCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/cli/RegisteredCommandCollectorTest.java new file mode 100644 index 0000000000..070ae922a2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/cli/RegisteredCommandCollectorTest.java @@ -0,0 +1,93 @@ +/* + * 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 com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.NamedClassElement; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.plugin.ScmModule; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RegisteredCommandCollectorTest { + + @Mock + private PluginLoader pluginLoader; + + @InjectMocks + private RegisteredCommandCollector commandCollector; + + @Test + void shouldCollectCommandsFromModulesAndPlugins() { + ScmModule module = mock(ScmModule.class); + when(pluginLoader.getInstalledModules()).thenReturn(ImmutableList.of(module)); + + when(module.getCliCommands()).thenReturn(ImmutableList.of(new NamedClassElement("moduleCommand", ModuleCommand.class.getName()))); + InstalledPlugin installedPlugin = mock(InstalledPlugin.class); + InstalledPluginDescriptor descriptor = mock(InstalledPluginDescriptor.class); + when(pluginLoader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedPlugin)); + when(installedPlugin.getDescriptor()).thenReturn(descriptor); + when(descriptor.getCliCommands()).thenReturn(ImmutableList.of(new NamedClassElement("subCommand", SubCommand.class.getName()))); + when(pluginLoader.getUberClassLoader()).thenReturn(RegisteredCommandCollectorTest.class.getClassLoader()); + + Set commands = commandCollector.collect(); + + assertThat(commands).hasSize(2); + assertThat(commands) + .map(RegisteredCommand::getName) + .containsExactlyInAnyOrder("subCommand", "moduleCommand"); + + List> commandClasses = commands.stream().map(RegisteredCommand::getCommand).collect(Collectors.toList()); + assertThat(commandClasses).containsExactlyInAnyOrder(SubCommand.class, ModuleCommand.class); + } + + static class ParentCommand { + + } + + @sonia.scm.cli.ParentCommand(value = ParentCommand.class) + static class SubCommand { + + } + + @sonia.scm.cli.ParentCommand(value = ParentCommand.class) + static class ModuleCommand { + + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryCreateCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryCreateCommandTest.java new file mode 100644 index 0000000000..91e5254e64 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryCreateCommandTest.java @@ -0,0 +1,108 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cli.CommandValidator; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryInitializer; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryTestData; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryCreateCommandTest { + + @Mock + private RepositoryManager manager; + @Mock + private RepositoryInitializer initializer; + @Mock + private RepositoryTemplateRenderer templateRenderer; + @Mock + private CommandValidator commandValidator; + + @InjectMocks + private RepositoryCreateCommand command; + + @Test + void shouldValidate() { + command.setType("git"); + command.setRepository("test/repo"); + command.run(); + + verify(commandValidator).validate(); + } + + @Test + void shouldCreateRepoWithoutInit() { + Repository heartOfGold = RepositoryTestData.createHeartOfGold(); + command.setType(heartOfGold.getType()); + command.setRepository(heartOfGold.getNamespaceAndName().toString()); + command.run(); + + verify(manager).create(argThat(repository -> { + assertThat(repository.getType()).isEqualTo(heartOfGold.getType()); + return true; + })); + verify(initializer, never()).initialize(eq(heartOfGold), anyMap()); + } + + @Test + void shouldCreateRepoWithInit() { + Repository puzzle = RepositoryTestData.create42Puzzle(); + when(manager.create(any())).thenReturn(puzzle); + command.setType(puzzle.getType()); + command.setRepository(puzzle.getNamespaceAndName().toString()); + command.setInit(true); + command.run(); + + verify(initializer).initialize(eq(puzzle), anyMap()); + } + + @Test + void shouldRenderTemplateAfterCreation() { + Repository puzzle = RepositoryTestData.create42Puzzle(); + when(manager.create(any())).thenReturn(puzzle); + + command.setType(puzzle.getType()); + command.setRepository(puzzle.getNamespaceAndName().toString()); + command.run(); + + verify(templateRenderer).render(puzzle); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryDeleteCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryDeleteCommandTest.java new file mode 100644 index 0000000000..ebbff19f2d --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryDeleteCommandTest.java @@ -0,0 +1,81 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cli.CliContext; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryTestData; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryDeleteCommandTest { + + @Mock + private RepositoryTemplateRenderer templateRenderer; + @Mock + private RepositoryManager manager; + + @InjectMocks + private RepositoryDeleteCommand command; + + @Test + void shouldRenderPromptWithoutYesFlag() { + command.setRepository("test/repo"); + command.run(); + + verify(templateRenderer).renderToStderr(any(), anyMap()); + } + + @Test + void shouldExitOnInvalidInput() { + command.setRepository("test"); + command.setShouldDelete(true); + command.run(); + + verify(templateRenderer).renderInvalidInputError(); + } + + @Test + void shouldDeleteRepository() { + Repository puzzle = RepositoryTestData.create42Puzzle(); + when(manager.get(new NamespaceAndName("test", "r"))).thenReturn(puzzle); + command.setRepository("test/r"); + command.setShouldDelete(true); + command.run(); + + verify(manager).delete(puzzle); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryGetCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryGetCommandTest.java new file mode 100644 index 0000000000..07d12059cc --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryGetCommandTest.java @@ -0,0 +1,88 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryTestData; +import sonia.scm.repository.cli.RepositoryGetCommand; +import sonia.scm.repository.cli.RepositoryTemplateRenderer; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryGetCommandTest { + + @Mock + private RepositoryManager repositoryManager; + @Mock + private RepositoryTemplateRenderer templateRenderer; + + @InjectMocks + private RepositoryGetCommand command; + + @Test + void shouldRenderNotFoundError() { + String repo = "test/repo"; + + when(repositoryManager.get(new NamespaceAndName("test", "repo"))).thenReturn(null); + + command.setRepository(repo); + command.run(); + + verify(templateRenderer).renderNotFoundError(); + } + + @Test + void shouldRenderInvalidInputError() { + String repo = "repo"; + + command.setRepository(repo); + command.run(); + + verify(templateRenderer).renderInvalidInputError(); + } + + @Test + void shouldRenderTemplateToStdout() { + String repo = "test/repo"; + Repository puzzle = RepositoryTestData.create42Puzzle(); + when(repositoryManager.get(new NamespaceAndName("test", "repo"))).thenReturn(puzzle); + + command.setRepository(repo); + command.run(); + + verify(templateRenderer).render(puzzle); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryListCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryListCommandTest.java new file mode 100644 index 0000000000..33481e0cec --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryListCommandTest.java @@ -0,0 +1,89 @@ +/* + * 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.repository.cli; + +import com.google.common.collect.ImmutableMap; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cli.Table; +import sonia.scm.cli.TemplateRenderer; +import sonia.scm.repository.RepositoryManager; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryListCommandTest { + + @Mock + private TemplateRenderer templateRenderer; + @Mock + private RepositoryManager manager; + @Mock + private RepositoryToRepositoryCommandDtoMapper mapper; + + @InjectMocks + private RepositoryListCommand command; + + @Test + void shouldReturnShortTemplate() { + command.setShortTemplate(true); + command.run(); + + verify(templateRenderer, never()).createTable(); + verify(templateRenderer).renderToStdout(any(), anyMap()); + } + + @Test + void shouldReturnTableTemplate() { + Table table = mock(Table.class); + when(templateRenderer.createTable()).thenReturn(table); + + ArgumentCaptor> mapCaptor = ArgumentCaptor.forClass(Map.class); + ArgumentCaptor templateCaptor = ArgumentCaptor.forClass(String.class); + doNothing().when(templateRenderer).renderToStdout(templateCaptor.capture(), mapCaptor.capture()); + command.run(); + + Map map = mapCaptor.getValue(); + assertThat(map).hasSize(2); + assertThat(map.get("rows")).isInstanceOf(Table.class); + assertThat(map.get("repos")).isInstanceOf(List.class); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryModifyCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryModifyCommandTest.java new file mode 100644 index 0000000000..478c4a7fde --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryModifyCommandTest.java @@ -0,0 +1,90 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cli.CommandValidator; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryTestData; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryModifyCommandTest { + + @Mock + private RepositoryTemplateRenderer templateRenderer; + @Mock + private CommandValidator validator; + @Mock + private RepositoryManager manager; + + @InjectMocks + private RepositoryModifyCommand command; + + @Test + void shouldValidateParameters() { + command.setRepository("test/repo"); + command.run(); + + verify(validator).validate(); + } + + @Test + void shouldRenderInvalidInputError() { + command.setRepository("test"); + command.run(); + + verify(templateRenderer).renderInvalidInputError(); + } + + @Test + void shouldRenderNotFoundError() { + when(manager.get(new NamespaceAndName("test", "repo"))).thenReturn(null); + command.setRepository("test/repo"); + command.run(); + + verify(templateRenderer).renderNotFoundError(); + } + + @Test + void shouldModifyRepository() { + Repository puzzle = RepositoryTestData.create42Puzzle(); + when(manager.get(new NamespaceAndName("test", "repo"))).thenReturn(puzzle); + command.setRepository("test/repo"); + command.run(); + + verify(manager).modify(puzzle); + verify(templateRenderer).render(puzzle); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapperTest.java new file mode 100644 index 0000000000..2d784769e3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapperTest.java @@ -0,0 +1,95 @@ +/* + * 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.repository.cli; + +import com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryTestData; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.api.ScmProtocol; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryToRepositoryCommandDtoMapperTest { + + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService service; + + private RepositoryToRepositoryCommandDtoMapperImpl mapper; + + @BeforeEach + void initMapper() { + mapper = new RepositoryToRepositoryCommandDtoMapperImpl(); + mapper.setServiceFactory(serviceFactory); + } + + @Test + void shouldMapAttributes() { + Repository testRepo = RepositoryTestData.create42Puzzle(); + when(serviceFactory.create(testRepo)).thenReturn(service); + RepositoryCommandDto dto = mapper.map(testRepo); + + assertThat(dto.getNamespace()).isEqualTo(testRepo.getNamespace()); + assertThat(dto.getName()).isEqualTo(testRepo.getName()); + assertThat(dto.getContact()).isEqualTo(testRepo.getContact()); + assertThat(dto.getDescription()).isEqualTo(testRepo.getDescription()); + } + + @Test + void shouldAppendHttpUrl() { + ScmProtocol scmProtocol = new ScmProtocol() { + @Override + public String getType() { + return "http"; + } + + @Override + public String getUrl() { + return "http://localhost:8081/scm"; + } + }; + Repository testRepo = RepositoryTestData.create42Puzzle(); + + RepositoryService service = mock(RepositoryService.class); + when(serviceFactory.create(testRepo)).thenReturn(service); + when(service.getSupportedProtocols()).thenReturn(ImmutableList.of(scmProtocol).stream()); + + RepositoryCommandDto dto = mapper.map(testRepo); + + assertThat(dto.getUrl()).isEqualTo("http://localhost:8081/scm"); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/validation/DefaultValidatorProviderTest.java b/scm-webapp/src/test/java/sonia/scm/validation/DefaultValidatorProviderTest.java new file mode 100644 index 0000000000..2feabbe69c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/validation/DefaultValidatorProviderTest.java @@ -0,0 +1,78 @@ +/* + * 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.validation; + +import org.junit.jupiter.api.Test; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.Validator; +import javax.validation.constraints.Size; + +import static org.assertj.core.api.Assertions.assertThat; + +class DefaultValidatorProviderTest { + + @Test + void shouldCreateValidatorWithConstraintValidatorFactory() { + TestingConstraintValidatorFactory constraintValidatorFactory = new TestingConstraintValidatorFactory(); + DefaultValidatorProvider provider = new DefaultValidatorProvider(constraintValidatorFactory); + Validator validator = provider.get(); + validator.validate(new Sample("one")); + + assertThat(constraintValidatorFactory.counter).isOne(); + } + + private static class TestingConstraintValidatorFactory implements ConstraintValidatorFactory { + + private int counter = 0; + + @Override + public > T getInstance(Class key) { + counter++; + try { + return key.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void releaseInstance(ConstraintValidator instance) { + + } + } + + private static class Sample { + + @Size(max = 20) + private final String value; + + public Sample(String value) { + this.value = value; + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/validation/GuiceConstraintValidatorFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/validation/GuiceConstraintValidatorFactoryTest.java new file mode 100644 index 0000000000..5bd6835241 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/validation/GuiceConstraintValidatorFactoryTest.java @@ -0,0 +1,51 @@ +/* + * 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.validation; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.RepositoryManager; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class GuiceConstraintValidatorFactoryTest { + + @Mock + private RepositoryManager repositoryManager; + + @Test + void shouldUseInjectorToCreateConstraintInstance() { + Injector injector = Guice.createInjector(new RepositoryManagerModule(repositoryManager)); + GuiceConstraintValidatorFactory factory = new GuiceConstraintValidatorFactory(injector); + RepositoryTypeConstraintValidator instance = factory.getInstance(RepositoryTypeConstraintValidator.class); + assertThat(instance.getRepositoryManager()).isSameAs(repositoryManager); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/validation/RepositoryManagerModule.java b/scm-webapp/src/test/java/sonia/scm/validation/RepositoryManagerModule.java new file mode 100644 index 0000000000..7beb8a002e --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/validation/RepositoryManagerModule.java @@ -0,0 +1,42 @@ +/* + * 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.validation; + +import com.google.inject.AbstractModule; +import sonia.scm.repository.RepositoryManager; + +public class RepositoryManagerModule extends AbstractModule { + + private final RepositoryManager repositoryManager; + + public RepositoryManagerModule(RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + } + + @Override + protected void configure() { + bind(RepositoryManager.class).toInstance(repositoryManager); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/validation/RepositoryType.java b/scm-webapp/src/test/java/sonia/scm/validation/RepositoryType.java new file mode 100644 index 0000000000..af0eb4f55a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/validation/RepositoryType.java @@ -0,0 +1,43 @@ +/* + * 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.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +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; + +@Documented +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = RepositoryTypeConstraintValidator.class) +public @interface RepositoryType { + String message() default "Invalid type"; + Class[] groups() default { }; + Class[] payload() default { }; +} diff --git a/scm-webapp/src/test/java/sonia/scm/validation/RepositoryTypeConstraintValidator.java b/scm-webapp/src/test/java/sonia/scm/validation/RepositoryTypeConstraintValidator.java new file mode 100644 index 0000000000..237bf93b05 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/validation/RepositoryTypeConstraintValidator.java @@ -0,0 +1,51 @@ +/* + * 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.validation; + +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Inject; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class RepositoryTypeConstraintValidator implements ConstraintValidator { + + private final RepositoryManager repositoryManager; + + @Inject + public RepositoryTypeConstraintValidator(RepositoryManager repositoryManager) { + this.repositoryManager = repositoryManager; + } + + public RepositoryManager getRepositoryManager() { + return repositoryManager; + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return repositoryManager.getConfiguredTypes() + .stream().anyMatch(t -> t.getName().equalsIgnoreCase(value)); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/validation/ValidationModuleTest.java b/scm-webapp/src/test/java/sonia/scm/validation/ValidationModuleTest.java new file mode 100644 index 0000000000..5104229a3f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/validation/ValidationModuleTest.java @@ -0,0 +1,132 @@ +/* + * 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.validation; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Guice; +import com.google.inject.Injector; +import lombok.AllArgsConstructor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Inject; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import java.util.Collections; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +class ValidationModuleTest { + + @Nested + class SimpleInjectionTests { + + private Injector injector; + + @BeforeEach + void setUpInjector() { + injector = Guice.createInjector(new ValidationModule()); + } + + @Test + void shouldInjectValidator() { + WithValidator instance = injector.getInstance(WithValidator.class); + assertThat(instance.validator).isNotNull(); + } + + @Test + void shouldInjectConstraintValidatorFactory() { + WithConstraintValidatorFactory instance = injector.getInstance(WithConstraintValidatorFactory.class); + assertThat(instance.constraintValidatorFactory).isInstanceOf(GuiceConstraintValidatorFactory.class); + } + + } + + @Nested + @ExtendWith(MockitoExtension.class) + class RealWorldTests { + + @Mock + private RepositoryManager repositoryManager; + + @Test + void shouldValidateRepositoryTypes() { + when(repositoryManager.getConfiguredTypes()).thenReturn(ImmutableList.of( + new sonia.scm.repository.RepositoryType("git", "Git", Collections.emptySet()), + new sonia.scm.repository.RepositoryType("hg", "Mercurial", Collections.emptySet()) + )); + + Injector injector = Guice.createInjector(new ValidationModule(), new RepositoryManagerModule(repositoryManager)); + + Validator validator = injector.getInstance(Validator.class); + + Set> violations = validator.validate(new Repository("svn")); + assertThat(violations).isNotEmpty(); + + violations = validator.validate(new Repository("git")); + assertThat(violations).isEmpty(); + } + + } + + + public static class WithValidator { + + private final Validator validator; + + @Inject + public WithValidator(Validator validator) { + this.validator = validator; + } + } + + public static class WithConstraintValidatorFactory { + + private final ConstraintValidatorFactory constraintValidatorFactory; + + @Inject + public WithConstraintValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory) { + this.constraintValidatorFactory = constraintValidatorFactory; + } + } + + + @AllArgsConstructor + public static class Repository { + + @RepositoryType + private String type; + + } + +} diff --git a/scm-webapp/src/test/resources/sonia/scm/cli/test.properties b/scm-webapp/src/test/resources/sonia/scm/cli/test.properties new file mode 100644 index 0000000000..c4ceca4815 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/cli/test.properties @@ -0,0 +1,24 @@ +# +# 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. +# +scm.one.two.three.usage.header = Bla This is my app. It does stuff. Good stuff.%n diff --git a/scm-webapp/src/test/resources/sonia/scm/cli/test_de.properties b/scm-webapp/src/test/resources/sonia/scm/cli/test_de.properties new file mode 100644 index 0000000000..004b0cfbb5 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/cli/test_de.properties @@ -0,0 +1,25 @@ +# +# 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. +# + +scm.one.two.three.usage.header = Bla Dies ist meine App. Funktioniert.%n