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

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