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