mirror of
https://gitee.com/dcren/initializr.git
synced 2025-11-08 10:24:58 +08:00
Improve command-line service capabilities
Provide a polished version of the command-line service capabilities with tables and support for the new version range. Also support SpringBootCli explicitly so that the service capabilities are rendered directly by the server instead of having the client parsing the meta-data manually. Closes gh-76
This commit is contained in:
@@ -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}.
|
||||
* <p>
|
||||
* 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
${logo}
|
||||
:: Service capabilities :: ${serviceUrl}
|
||||
|
||||
Supported dependencies
|
||||
${dependencies}
|
||||
|
||||
Project types (* denotes the default)
|
||||
${types}
|
||||
|
||||
Parameters
|
||||
${parameters}
|
||||
@@ -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}
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -252,6 +252,19 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
|
||||
validateGenericHelpContent(response)
|
||||
}
|
||||
|
||||
@Test
|
||||
void springBootCliReceivesJsonByDefault() {
|
||||
ResponseEntity<String> response = invokeHome('SpringBootCli/1.2.0', "*/*")
|
||||
validateContentType(response, CURRENT_METADATA_MEDIA_TYPE)
|
||||
validateCurrentMetadata(new JSONObject(response.body))
|
||||
}
|
||||
|
||||
@Test
|
||||
void springBootCliWithAcceptHeaderText() {
|
||||
ResponseEntity<String> 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<String> 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')
|
||||
|
||||
Reference in New Issue
Block a user