diff --git a/initializr/src/main/groovy/io/spring/initializr/CommandLineHelpGenerator.groovy b/initializr/src/main/groovy/io/spring/initializr/CommandLineHelpGenerator.groovy index 8d06a941..c177bb39 100644 --- a/initializr/src/main/groovy/io/spring/initializr/CommandLineHelpGenerator.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/CommandLineHelpGenerator.groovy @@ -16,6 +16,8 @@ package io.spring.initializr +import io.spring.initializr.support.VersionRange + import static io.spring.initializr.support.GroovyTemplate.template /** @@ -42,10 +44,9 @@ class CommandLineHelpGenerator { String generateGenericCapabilities(InitializrMetadata metadata, String serviceUrl) { def model = initializeModel(metadata, serviceUrl) model['hasExamples'] = false - doGenerateCapabilities(model) + template 'cli-capabilities.txt', model } - /** * Generate the capabilities of the service using "curl" as a plain text * document. @@ -54,7 +55,7 @@ class CommandLineHelpGenerator { def model = initializeModel(metadata, serviceUrl) model['examples'] = template 'curl-examples.txt', model model['hasExamples'] = true - doGenerateCapabilities(model) + template 'cli-capabilities.txt', model } /** @@ -65,43 +66,47 @@ class CommandLineHelpGenerator { def model = initializeModel(metadata, serviceUrl) model['examples'] = template 'httpie-examples.txt', model model['hasExamples'] = true - doGenerateCapabilities(model) - } - - private doGenerateCapabilities(def model) { template 'cli-capabilities.txt', model } + /** + * Generate the capabilities of the service using Spring Boot CLI as a plain + * text document. + */ + String generateSpringBootCliCapabilities(InitializrMetadata metadata, String serviceUrl) { + def model = initializeModel(metadata, serviceUrl) + model['hasExamples'] = false + template('boot-cli-capabilities.txt', model) + } private Map initializeModel(InitializrMetadata metadata, serviceUrl) { Map model = [:] model['logo'] = logo model['serviceUrl'] = serviceUrl - Map dependencies = [:] - new ArrayList(metadata.allDependencies).sort { a, b -> a.id <=> b.id }.each { - String description = it.name - if (it.description) { - description += ": $it.description" - } - if (it.versionRange) { - String range = it.versionRange.trim() - description += " - $range" - } - dependencies[it.id] = description + String[][] dependencyTable = new String[metadata.allDependencies.size() + 1][]; + dependencyTable[0] = ["Id", "Description", "Required version"] + new ArrayList(metadata.allDependencies).sort { a, b -> a.id <=> b.id } + .eachWithIndex { dep, i -> + String[] data = new String[3] + data[0] = dep.id + data[1] = dep.description ?: dep.name + data[2] = buildVersionRangeRepresentation(dep.versionRange) + dependencyTable[i + 1] = data } - model['dependencies'] = dependencies + model['dependencies'] = TableGenerator.generate(dependencyTable) - Map types = [:] - new ArrayList<>(metadata.types).sort { a, b -> a.id <=> b.id }.each { - String description = it.description - if (!description) { - description = it.name - } - types[it.id] = description + String[][] typeTable = new String[metadata.types.size() + 1][]; + typeTable[0] = ["Id", "Description", "Tags"] + new ArrayList<>(metadata.types).sort { a, b -> a.id <=> b.id }.eachWithIndex { type, i -> + String[] data = new String[3] + data[0] = (metadata.defaults.type.equals(type.id) ? type.id + " *" : type.id) + data[1] = type.description ?: type.name + data[2] = buildTagRepresentation(type) + typeTable[i + 1] = data; } - model['types'] = types + model['types'] = TableGenerator.generate(typeTable) Map defaults = [:] metadata.defaults.properties.sort().each { @@ -109,11 +114,117 @@ class CommandLineHelpGenerator { defaults[it.key] = it.value } } + String[][] parameterTable = new String[defaults.size() + 1][]; + parameterTable[0] = ["Id", "Default value"] + defaults.keySet().eachWithIndex { id, i -> + String[] data = new String[2] + data[0] = id + data[1] = metadata.defaults.properties[id] + parameterTable[i + 1] = data + } + model['parameters'] = TableGenerator.generate(parameterTable) + + defaults['applicationName'] = ProjectRequest.generateApplicationName(metadata.defaults.name, ProjectRequest.DEFAULT_APPLICATION_NAME) model['defaults'] = defaults + model } + private static String buildVersionRangeRepresentation(String range) { + if (!range) { + return null + } + VersionRange versionRange = VersionRange.parse(range) + if (versionRange.higherVersion == null) { + return ">= $range" + } else { + return range.trim() + } + } + + private static String buildTagRepresentation(InitializrMetadata.Type type) { + if (type.tags.isEmpty()) { + return ""; + } + type.tags.collect { key, value -> + "$key:$value" + }.join(",") + } + + private static class TableGenerator { + + static final String NEW_LINE = System.getProperty("line.separator") + + /** + * Generate a table description for the specified {@code content}. + *

+ * The {@code content} is a two-dimensional array holding the rows + * of the table. The first entry holds the header of the table. + */ + public static String generate(String[][] content) { + StringBuilder sb = new StringBuilder() + int[] columnsLength = computeColumnsLength(content) + appendTableSeparation(sb, columnsLength) + appendRow(sb, content, columnsLength, 0) // Headers + appendTableSeparation(sb, columnsLength) + for (int i = 1; i < content.length; i++) { + appendRow(sb, content, columnsLength, i) + } + appendTableSeparation(sb, columnsLength) + sb.toString() + } + + + private static void appendRow(StringBuilder sb, String[][] content, + int[] columnsLength, int rowIndex) { + String[] row = content[rowIndex] + for (int i = 0; i < row.length; i++) { + sb.append("| ").append(fill(row[i], columnsLength[i])).append(" ") + } + sb.append("|") + sb.append(NEW_LINE) + } + + private static void appendTableSeparation(StringBuilder sb, int[] headersLength) { + for (int headerLength : headersLength) { + sb.append("+").append("-".multiply(headerLength + 2)) + } + sb.append("+") + sb.append(NEW_LINE) + } + + private static String fill(String data, int columnSize) { + if (data == null) { + return " ".multiply(columnSize) + } else { + int i = columnSize - data.length() + return data + " ".multiply(i) + } + } + + private static int[] computeColumnsLength(String[][] content) { + int count = content[0].length + int[] result = new int[count] + for (int i = 0; i < count; i++) { + result[i] = largest(content, i) + } + return result + } + + private static int largest(String[][] content, int column) { + int max = 0 + for (String[] rows : content) { + String s = rows[column] + if (s && s.length() > max) { + max = s.length() + } + } + return max + } + + } + } diff --git a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy index 1101dcb4..af7cedcc 100644 --- a/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/InitializrMetadata.groovy @@ -248,6 +248,10 @@ class InitializrMetadata { String versionRange + void setVersionRange(String versionRange) { + this.versionRange = versionRange ? versionRange.trim() : null + } + /** * Specify if the dependency has its coordinates set, i.e. {@code groupId} * and {@code artifactId}. diff --git a/initializr/src/main/groovy/io/spring/initializr/support/VersionRange.groovy b/initializr/src/main/groovy/io/spring/initializr/support/VersionRange.groovy index de5a4097..546c3b67 100644 --- a/initializr/src/main/groovy/io/spring/initializr/support/VersionRange.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/support/VersionRange.groovy @@ -44,12 +44,19 @@ class VersionRange { private static final String RANGE_REGEX = "(\\(|\\[)(.*),(.*)(\\)|\\])" - private Version lowerVersion - private boolean lowerInclusive - private Version higherVersion - private boolean higherInclusive + final Version lowerVersion + final boolean lowerInclusive + final Version higherVersion + final boolean higherInclusive - /** + private VersionRange(Version lowerVersion, boolean lowerInclusive, + Version higherVersion, boolean higherInclusive) { + this.lowerVersion = lowerVersion + this.lowerInclusive = lowerInclusive + this.higherVersion = higherVersion + this.higherInclusive = higherInclusive + } +/** * Specify if the {@link Version} matches this range. Returns {@code true} * if the version is contained within this range, {@code false} otherwise. */ @@ -85,14 +92,13 @@ class VersionRange { if (!matcher.matches()) { // Try to read it as simple string Version version = Version.parse(text) - return new VersionRange(lowerInclusive: true, lowerVersion: version) + return new VersionRange(version, true, null, true) } - VersionRange range = new VersionRange() - range.lowerInclusive = matcher[0][1].equals('[') - range.lowerVersion = Version.parse(matcher[0][2]) - range.higherVersion = Version.parse(matcher[0][3]) - range.higherInclusive = matcher[0][4].equals(']') - range + boolean lowerInclusive = matcher[0][1].equals('[') + Version lowerVersion = Version.parse(matcher[0][2]) + Version higherVersion = Version.parse(matcher[0][3]) + boolean higherInclusive = matcher[0][4].equals(']') + new VersionRange(lowerVersion, lowerInclusive, higherVersion, higherInclusive) } } diff --git a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy index 37b42a33..32eda51b 100644 --- a/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/web/MainController.groovy @@ -76,6 +76,9 @@ class MainController extends AbstractInitializrController { if (userAgent.startsWith(WebConfig.HTTPIE_USER_AGENT_PREFIX)) { return builder.body(commandLineHelpGenerator.generateHttpieCapabilities(metadata, appUrl)) } + if (userAgent.startsWith(WebConfig.SPRING_BOOT_CLI_AGENT_PREFIX)) { + return builder.body(commandLineHelpGenerator.generateSpringBootCliCapabilities(metadata, appUrl)) + } } builder.body(commandLineHelpGenerator.generateGenericCapabilities(metadata, appUrl)) } diff --git a/initializr/src/main/groovy/io/spring/initializr/web/WebConfig.groovy b/initializr/src/main/groovy/io/spring/initializr/web/WebConfig.groovy index e9d6aec4..3379958a 100644 --- a/initializr/src/main/groovy/io/spring/initializr/web/WebConfig.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/web/WebConfig.groovy @@ -39,6 +39,8 @@ class WebConfig extends WebMvcConfigurerAdapter { static final String HTTPIE_USER_AGENT_PREFIX = 'HTTPie' + static final String SPRING_BOOT_CLI_AGENT_PREFIX = 'SpringBootCli' + @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.defaultContentTypeStrategy(new CommandLineContentNegotiationStrategy()) diff --git a/initializr/src/main/resources/templates/boot-cli-capabilities.txt b/initializr/src/main/resources/templates/boot-cli-capabilities.txt new file mode 100644 index 00000000..2cb5b791 --- /dev/null +++ b/initializr/src/main/resources/templates/boot-cli-capabilities.txt @@ -0,0 +1,11 @@ +${logo} +:: Service capabilities :: ${serviceUrl} + +Supported dependencies +${dependencies} + +Project types (* denotes the default) +${types} + +Parameters +${parameters} diff --git a/initializr/src/main/resources/templates/cli-capabilities.txt b/initializr/src/main/resources/templates/cli-capabilities.txt index 967e0f16..fd05456f 100644 --- a/initializr/src/main/resources/templates/cli-capabilities.txt +++ b/initializr/src/main/resources/templates/cli-capabilities.txt @@ -9,15 +9,13 @@ The services uses a HAL based hypermedia format to expose a set of resources to interact with. If you access this root resource requesting application/json as media type the response will contain the following links: -<% types.each {key, value -> %> -* ${key} - ${value} <% } %> +${types} The URI templates take a set of parameters to customize the result of a request to the linked resource. - +-----------------+------------------------------------------+----------------- | Parameter | Description | Default -|-----------------+------------------------------------------+----------------- ++-----------------+------------------------------------------+----------------- | groupId | project coordinates | ${defaults.groupId} | artifactId | project coordinates (infer archive name) | ${defaults.artifactId} | version | project version | ${defaults.version} @@ -36,13 +34,9 @@ a request to the linked resource. The following section has a list of supported identifiers for the comma separated list of "dependencies". -<% dependencies.each {key, value -> %> -${key} - ${value} <% } %> - - +${dependencies} <% if (hasExamples) { %> Examples: ${examples} - <% } %> diff --git a/initializr/src/test/groovy/io/spring/initializr/CommandLineHelpGeneratorTest.groovy b/initializr/src/test/groovy/io/spring/initializr/CommandLineHelpGeneratorTest.groovy index 560b2917..da41963d 100644 --- a/initializr/src/test/groovy/io/spring/initializr/CommandLineHelpGeneratorTest.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/CommandLineHelpGeneratorTest.groovy @@ -36,8 +36,8 @@ class CommandLineHelpGeneratorTest { createDependency('id-b', 'depB'), createDependency('id-a', 'depA', 'and some description')).validateAndGet() String content = generator.generateGenericCapabilities(metadata, "https://fake-service") - assertThat content, containsString('id-a - depA: and some description') - assertThat content, containsString('id-b - depB') + assertThat content, containsString('id-a | and some description |') + assertThat content, containsString('id-b | depB') assertThat content, containsString("https://fake-service") assertThat content, not(containsString('Examples:')) assertThat content, not(containsString('curl')) @@ -49,7 +49,9 @@ class CommandLineHelpGeneratorTest { .addType(new InitializrMetadata.Type(id: 'foo', name: 'foo-name', description: 'foo-desc')) .validateAndGet() String content = generator.generateGenericCapabilities(metadata, "https://fake-service") - assertThat content, containsString('foo - foo-desc') + assertThat content, containsString('| foo') + assertThat content, containsString('| foo-desc') + } @Test @@ -58,8 +60,8 @@ class CommandLineHelpGeneratorTest { createDependency('id-b', 'depB'), createDependency('id-a', 'depA', 'and some description')).validateAndGet() String content = generator.generateCurlCapabilities(metadata, "https://fake-service") - assertThat content, containsString('id-a - depA: and some description') - assertThat content, containsString('id-b - depB') + assertThat content, containsString('id-a | and some description |') + assertThat content, containsString('id-b | depB') assertThat content, containsString("https://fake-service") assertThat content, containsString('Examples:') assertThat content, containsString('curl') @@ -71,14 +73,40 @@ class CommandLineHelpGeneratorTest { createDependency('id-b', 'depB'), createDependency('id-a', 'depA', 'and some description')).validateAndGet() String content = generator.generateHttpieCapabilities(metadata, "https://fake-service") - assertThat content, containsString('id-a - depA: and some description') - assertThat content, containsString('id-b - depB') + assertThat content, containsString('id-a | and some description |') + assertThat content, containsString('id-b | depB') assertThat content, containsString("https://fake-service") assertThat content, containsString('Examples:') assertThat content, not(containsString('curl')) assertThat content, containsString("http https://fake-service") } + @Test + void generateSpringBootCliCapabilities() { + def metadata = InitializrMetadataBuilder.withDefaults().addDependencyGroup("test", + createDependency('id-b', 'depB'), + createDependency('id-a', 'depA', 'and some description')).validateAndGet() + String content = generator.generateSpringBootCliCapabilities(metadata, "https://fake-service") + assertThat content, containsString('id-a | and some description |') + assertThat content, containsString('id-b | depB') + assertThat content, containsString("https://fake-service") + assertThat content, not(containsString('Examples:')) + assertThat content, not(containsString('curl')) + } + + @Test + void generateCapabilitiesWithVersionRange() { + InitializrMetadata.Dependency first = new InitializrMetadata.Dependency( + id: 'first', description: 'first desc', versionRange: '1.2.0.RELEASE') + InitializrMetadata.Dependency second = new InitializrMetadata.Dependency( + id: 'second', description: 'second desc', versionRange: ' [1.2.0.RELEASE,1.3.0.M1) ') + def metadata = InitializrMetadataBuilder.withDefaults().addDependencyGroup("test", first, second).validateAndGet() + String content = generator.generateSpringBootCliCapabilities(metadata, "https://fake-service") + assertThat content, containsString('| first | first desc | >= 1.2.0.RELEASE |') + assertThat content, containsString('| second | second desc | [1.2.0.RELEASE,1.3.0.M1) |') + + } + private static def createDependency(String id, String name) { createDependency(id, name, null) } diff --git a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy index 14ca28dd..cc4597ab 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/MainControllerIntegrationTests.groovy @@ -252,6 +252,19 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra validateGenericHelpContent(response) } + @Test + void springBootCliReceivesJsonByDefault() { + ResponseEntity response = invokeHome('SpringBootCli/1.2.0', "*/*") + validateContentType(response, CURRENT_METADATA_MEDIA_TYPE) + validateCurrentMetadata(new JSONObject(response.body)) + } + + @Test + void springBootCliWithAcceptHeaderText() { + ResponseEntity response = invokeHome('SpringBootCli/1.2.0', "text/plain") + validateSpringBootHelpContent(response) + } + @Test // Test that the current output is exactly what we expect void validateCurrentProjectMetadata() { def json = getMetadataJson() @@ -301,6 +314,15 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra not(containsString("curl")))) } + private void validateSpringBootHelpContent(ResponseEntity response) { + validateContentType(response, MediaType.TEXT_PLAIN) + assertThat(response.body, allOf( + containsString("Service capabilities"), + containsString("Supported dependencies"), + not(containsString('Examples:')), + not(containsString("curl")))) + } + @Test void metricsAvailableByDefault() { downloadZip('/starter.zip?packaging=jar&javaVersion=1.8&style=web&style=jpa')