CLI Support for repository actions (#1987)

To make SCM-Manager more accessible and to make it easier using scripts against the server, we created a command line interface. This command line interface can be used to perform the default actions like create, modify and delete repositories. It is also very flexible and can be extended by plugins.

The CLI already supports internationalization, help texts, input validation, loose and table-like templates and nested subcommands. Check the cli guidelines to learn how add new cli commands.

Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2022-04-04 12:02:16 +02:00
committed by GitHub
parent 07afe4b439
commit 162dd6ad0a
90 changed files with 5303 additions and 21 deletions

View File

@@ -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();
...
}

View File

@@ -0,0 +1,2 @@
- type: added
description: Add cli support with repository actions ([#1987](https://github.com/scm-manager/scm-manager/pull/1987))

View File

@@ -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}",

View File

@@ -37,6 +37,9 @@ dependencies {
// rest api
implementation libraries.jaxRs
// cli
implementation libraries.picocli
// mapper
implementation libraries.mapstruct

View File

@@ -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

View File

@@ -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<ClassAnnotation> 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<? extends TypeElement> 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;

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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 <a href="https://beanvalidation.org/2.0/spec/">Bean validation spec</a>
* @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<ConstraintViolation<Object>> violations = validator.validate(spec.userObject());
if (!violations.isEmpty()) {
StringBuilder errorMsg = new StringBuilder();
for (ConstraintViolation<Object> 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);
}
}
}

View File

@@ -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() {}
}

View File

@@ -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<Table.Row> {
private static final String DEFAULT_LABEL_VALUE_SEPARATOR = ": ";
private final List<String[]> 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<Row> getRows() {
Map<Integer, Integer> maxLength = calculateMaxLength();
List<Row> rows = new ArrayList<>();
for (int r = 0; r < data.size(); r++) {
String[] rowArray = data.get(r);
List<Cell> 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<Integer, Integer> calculateMaxLength() {
Map<Integer, Integer> 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<Row> 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<Cell> cols;
}
}

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> model) {
Map<String, Object> finalModel = new HashMap<>(model);
finalModel.put("lf", "\n");
UnaryOperator<String> 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<String, String> {
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<Entry<String, String>> entrySet() {
throw new UnsupportedOperationException("Should not be used");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -58,6 +58,9 @@ public class ScmModule {
@XmlElement(name = "rest-resource")
private Set<ClassElement> restResources;
@XmlElement(name = "cli-command")
private Set<NamedClassElement> cliCommands;
@XmlElement(name = "mapper")
private Set<ClassElement> mappers;
@@ -87,6 +90,10 @@ public class ScmModule {
return nonNull(restResources);
}
public Iterable<NamedClassElement> getCliCommands() {
return nonNull(cliCommands);
}
public Iterable<ClassElement> getMappers() {
return nonNull(mappers);
}

View File

@@ -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<? extends Payload>[] 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;
}
}

View File

@@ -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<RepositoryName, String> {
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);
}
}

View File

@@ -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<? extends Payload>[] payload() default { };
}

View File

@@ -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<RepositoryTypeConstraint, String> {
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(", "));
}
}

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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<EFBFBD>ltiger Repository-Typ, bitte verwenden Sie einen der folgenden:
sonia.scm.repository.RepositoryName.message = Ung<EFBFBD>ltiger Repository Name

View File

@@ -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> K create(Class<K> 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 extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
try {
return key.getConstructor().newInstance();
} catch (Exception e) {
throw new IllegalStateException("Failed to create constraint validator");
}
}
@Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
}
}
}

View File

@@ -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());
}
}

View File

@@ -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 <T> Set<ConstraintViolation<T>> 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;
}
}
}

View File

@@ -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<ConstraintViolation<Repo>> 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<ConstraintViolation<Repo>> 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<ConstraintViolation<Repo>> violations = validator.validate(new Repo("hg"));
assertThat(violations).isEmpty();
}
@Test
@DefaultLocale("en")
void shouldAddAvailableTypesToMessage() {
mockRepositoryTypes("git", "hg", "svn");
Validator validator = validator();
Set<ConstraintViolation<Repo>> 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<RepositoryType> 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 extends ConstraintValidator<?, ?>> T getInstance(Class<T> 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) {
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<String> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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> K create(Class<K> 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);
}
}
}

View File

@@ -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<RegisteredCommandNode> createCommandTree() {
Set<RegisteredCommandNode> rootCommands = new HashSet<>();
Set<RegisteredCommand> registeredCommands = commandCollector.collect();
Map<Class<?>, 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;
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}
}

View File

@@ -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 {}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}

View File

@@ -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<RegisteredCommand> collect() {
Set<RegisteredCommand> 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<RegisteredCommand> commands, Iterable<? extends ScmModule> 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;
}
}
}

View File

@@ -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<RegisteredCommandNode> children = new ArrayList<>();
public RegisteredCommandNode(String name, Class<?> command) {
this.name = name;
this.command = command;
}
}

View File

@@ -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 {}

View File

@@ -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<Module> createModules(ClassOverrides overrides) {
List<Module> moduleList = new ArrayList<>();
moduleList.add(new ValidationModule());
moduleList.add(new ResteasyModule());
moduleList.add(ShiroWebModule.guiceFilterModule());
moduleList.add(new WebElementModule(pluginLoader));

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<RepositoryCommandDto> 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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<ScmProtocol> 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;
}
}

View File

@@ -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) {

View File

@@ -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<Validator> {
private final ConstraintValidatorFactory constraintValidatorFactory;
@Inject
public DefaultValidatorProvider(ConstraintValidatorFactory constraintValidatorFactory) {
this.constraintValidatorFactory = constraintValidatorFactory;
}
@Override
public Validator get() {
return Validation.buildDefaultValidatorFactory()
.usingContext()
.constraintValidatorFactory(constraintValidatorFactory)
.getValidator();
}
}

View File

@@ -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 extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
return injector.getInstance(key);
}
@Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
// do nothing
}
}

View File

@@ -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<ExecutableType> 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<Locale> locales = request.getHttpHeaders().getAcceptableLanguages();
return locales == null || locales.isEmpty() ? Locale.ENGLISH : locales.get(0);
}
}

View File

@@ -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<GeneralValidator> {
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()
);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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<50>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<73>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<EFBFBD>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<EFBFBD>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<64>ltig zu l<>schen
repoDeletePrompt= Wenn dieses Repository endg<64>ltig gel<65>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

View File

@@ -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() {
}
}
}

View File

@@ -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<RegisteredCommandNode> 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<RegisteredCommandNode> 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<RegisteredCommandNode> 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<RegisteredCommandNode> nodes, Class... expected) {
assertThat(nodes).map(RegisteredCommandNode::getCommand).contains(expected);
}
}

View File

@@ -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);
}
}

View File

@@ -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<RegisteredCommand> commands = commandCollector.collect();
assertThat(commands).hasSize(2);
assertThat(commands)
.map(RegisteredCommand::getName)
.containsExactlyInAnyOrder("subCommand", "moduleCommand");
List<Class<?>> 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 {
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<Map<String, Object>> mapCaptor = ArgumentCaptor.forClass(Map.class);
ArgumentCaptor<String> templateCaptor = ArgumentCaptor.forClass(String.class);
doNothing().when(templateRenderer).renderToStdout(templateCaptor.capture(), mapCaptor.capture());
command.run();
Map<String, Object> map = mapCaptor.getValue();
assertThat(map).hasSize(2);
assertThat(map.get("rows")).isInstanceOf(Table.class);
assertThat(map.get("repos")).isInstanceOf(List.class);
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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 extends ConstraintValidator<?, ?>> T getInstance(Class<T> 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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<? extends Payload>[] payload() default { };
}

View File

@@ -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<RepositoryType, String> {
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));
}
}

View File

@@ -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<ConstraintViolation<Repository>> 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;
}
}

View File

@@ -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

View File

@@ -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