Rework getting started section

- With build add feature to resolve dep versions which
  can be used in docs.
- More polished maven/gradle example.
- Relates #527
This commit is contained in:
Janne Valkealahti
2022-09-14 09:09:25 +01:00
parent 754e00d0ae
commit 15f03b358f
7 changed files with 314 additions and 58 deletions

View File

@@ -33,6 +33,7 @@ import org.asciidoctor.gradle.jvm.AsciidoctorTask;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.plugins.JavaLibraryPlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.PluginManager;
@@ -57,20 +58,26 @@ class DocsPlugin implements Plugin<Project> {
pluginManager.apply(SpringMavenPlugin.class);
pluginManager.apply(AsciidoctorJPlugin.class);
configureAdocPlugins(project);
ExtractVersionConstraints dependencyVersions = project.getTasks().create("dependencyVersions",
ExtractVersionConstraints.class, task -> {
task.enforcedPlatform(":spring-shell-management");
});
configureAdocPlugins(project, dependencyVersions);
project.getTasks().withType(GenerateModuleMetadata.class, metadata -> {
metadata.setEnabled(false);
});
}
private void configureAdocPlugins(Project project) {
private void configureAdocPlugins(Project project, ExtractVersionConstraints dependencyVersions) {
project.getPlugins().withType(AsciidoctorJPlugin.class, (asciidoctorPlugin) -> {
createDefaultAsciidoctorRepository(project);
makeAllWarningsFatal(project);
Sync unzipResources = createUnzipDocumentationResourcesTask(project);
Sync snippetsResources = createSnippetsResourcesTask(project);
project.getTasks().withType(AbstractAsciidoctorTask.class, (asciidoctorTask) -> {
asciidoctorTask.dependsOn(dependencyVersions);
asciidoctorTask.dependsOn(unzipResources);
asciidoctorTask.dependsOn(snippetsResources);
// configureExtensions(project, asciidoctorTask);
@@ -94,12 +101,13 @@ class DocsPlugin implements Plugin<Project> {
// For now copy the entire sourceDir over so that include files are
// available in the intermediateWorkDir
resourcesSrcDirSpec.include("images/*");
resourcesSrcDirSpec.include("code/*");
}
});
}
});
if (asciidoctorTask instanceof AsciidoctorTask) {
configureHtmlOnlyAttributes(project, asciidoctorTask);
configureHtmlOnlyAttributes(project, asciidoctorTask, dependencyVersions);
}
});
});
@@ -179,7 +187,26 @@ class DocsPlugin implements Plugin<Project> {
asciidoctorTask.options(Collections.singletonMap("doctype", "book"));
}
private void configureHtmlOnlyAttributes(Project project, AbstractAsciidoctorTask asciidoctorTask) {
private void configureHtmlOnlyAttributes(Project project, AbstractAsciidoctorTask asciidoctorTask,
ExtractVersionConstraints dependencyVersions) {
asciidoctorTask.doFirst(new Action<Task>() {
@Override
public void execute(Task arg0) {
asciidoctorTask.getAttributeProviders().add(new AsciidoctorAttributeProvider() {
@Override
public Map<String, Object> getAttributes() {
Map<String, String> versionConstraints = dependencyVersions.getVersionConstraints();
Map<String, Object> attrs = new HashMap<>();
attrs.put("spring-version", versionConstraints.get("org.springframework:spring-core"));
attrs.put("spring-boot-version", versionConstraints.get("org.springframework.boot:spring-boot"));
return attrs;
}
});
}
});
Map<String, Object> attributes = new HashMap<>();
attributes.put("toc", "left");
attributes.put("source-highlighter", "highlight.js");
@@ -195,8 +222,7 @@ class DocsPlugin implements Plugin<Project> {
Object version = project.getVersion();
Map<String, Object> attrs = new HashMap<>();
if (version != null && version.toString() != Project.DEFAULT_VERSION) {
attrs.put("revnumber", version);
attrs.put("projectVersion", version);
attrs.put("project-version", version);
}
return attrs;
}

View File

@@ -0,0 +1,154 @@
package org.springframework.shell.gradle;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.gradle.api.DefaultTask;
import org.gradle.api.artifacts.ComponentMetadataDetails;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.DependencyConstraint;
import org.gradle.api.artifacts.DependencyConstraintMetadata;
import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.TaskAction;
/**
* @author Janne Valkealahti
*/
class ExtractVersionConstraints extends DefaultTask {
private final Configuration configuration;
private final Map<String, String> versionConstraints = new TreeMap<>();
private final Set<ConstrainedVersion> constrainedVersions = new TreeSet<>();
private final Set<VersionProperty> versionProperties = new TreeSet<>();
private final List<String> projectPaths = new ArrayList<>();
public ExtractVersionConstraints() {
DependencyHandler dependencies = getProject().getDependencies();
this.configuration = getProject().getConfigurations().create(getName());
dependencies.getComponents().all(this::processMetadataDetails);
}
public void enforcedPlatform(String projectPath) {
this.configuration.getDependencies().add(getProject().getDependencies().enforcedPlatform(
getProject().getDependencies().project(Collections.singletonMap("path", projectPath))));
this.projectPaths.add(projectPath);
}
@Internal
public Map<String, String> getVersionConstraints() {
return Collections.unmodifiableMap(this.versionConstraints);
}
@Internal
public Set<ConstrainedVersion> getConstrainedVersions() {
return this.constrainedVersions;
}
@Internal
public Set<VersionProperty> getVersionProperties() {
return this.versionProperties;
}
@TaskAction
void extractVersionConstraints() {
this.configuration.resolve();
for (String projectPath : this.projectPaths) {
for (DependencyConstraint constraint : getProject().project(projectPath).getConfigurations()
.getByName("apiElements").getAllDependencyConstraints()) {
this.versionConstraints.put(constraint.getGroup() + ":" + constraint.getName(),
constraint.getVersionConstraint().toString());
this.constrainedVersions.add(new ConstrainedVersion(constraint.getGroup(), constraint.getName(),
constraint.getVersionConstraint().toString()));
}
}
}
private void processMetadataDetails(ComponentMetadataDetails details) {
details.allVariants((variantMetadata) -> variantMetadata.withDependencyConstraints((dependencyConstraints) -> {
for (DependencyConstraintMetadata constraint : dependencyConstraints) {
this.versionConstraints.put(constraint.getGroup() + ":" + constraint.getName(),
constraint.getVersionConstraint().toString());
this.constrainedVersions.add(new ConstrainedVersion(constraint.getGroup(), constraint.getName(),
constraint.getVersionConstraint().toString()));
}
}));
}
public static final class ConstrainedVersion implements Comparable<ConstrainedVersion>, Serializable {
private final String group;
private final String artifact;
private final String version;
private ConstrainedVersion(String group, String artifact, String version) {
this.group = group;
this.artifact = artifact;
this.version = version;
}
public String getGroup() {
return this.group;
}
public String getArtifact() {
return this.artifact;
}
public String getVersion() {
return this.version;
}
@Override
public int compareTo(ConstrainedVersion other) {
int groupComparison = this.group.compareTo(other.group);
if (groupComparison != 0) {
return groupComparison;
}
return this.artifact.compareTo(other.artifact);
}
}
public static final class VersionProperty implements Comparable<VersionProperty>, Serializable {
private final String libraryName;
private final String versionProperty;
public VersionProperty(String libraryName, String versionProperty) {
this.libraryName = libraryName;
this.versionProperty = versionProperty;
}
public String getLibraryName() {
return this.libraryName;
}
public String getVersionProperty() {
return this.versionProperty;
}
@Override
public int compareTo(VersionProperty other) {
int groupComparison = this.libraryName.compareToIgnoreCase(other.libraryName);
if (groupComparison != 0) {
return groupComparison;
}
return this.versionProperty.compareTo(other.versionProperty);
}
}
}

View File

@@ -0,0 +1,27 @@
$ $JAVA_HOME/bin/java -jar demo-0.0.1-SNAPSHOT.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v{spring-boot-version})
2022-09-13T18:42:12.818+01:00 INFO 12644 --- [ main] com.example.demo.DemoApplication
: Starting DemoApplication using Java 17.0.4 on ...
2022-09-13T18:42:12.821+01:00 INFO 12644 --- [ main] com.example.demo.DemoApplication
: No active profile set, falling back to 1 default profile: "default"
2022-09-13T18:42:13.606+01:00 INFO 12644 --- [ main] com.example.demo.DemoApplication
: Started DemoApplication in 1.145 seconds (process running for 1.578)
shell:>help
AVAILABLE COMMANDS
Built-In Commands
help: Display help about available commands
stacktrace: Display the full stacktrace of the last error.
clear: Clear the shell screen.
quit, exit: Exit the shell.
history: Display or save the history of previously run commands
version: Show version info
script: Read and execute commands from a file.

View File

@@ -0,0 +1,26 @@
$JAVA_HOME/bin/java -jar demo-0.0.1-SNAPSHOT.jar help
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v{spring-boot-version})
2022-09-13T18:42:12.818+01:00 INFO 12644 --- [ main] com.example.demo.DemoApplication
: Starting DemoApplication using Java 17.0.4 on ...
2022-09-13T18:42:12.821+01:00 INFO 12644 --- [ main] com.example.demo.DemoApplication
: No active profile set, falling back to 1 default profile: "default"
2022-09-13T18:42:13.606+01:00 INFO 12644 --- [ main] com.example.demo.DemoApplication
: Started DemoApplication in 1.145 seconds (process running for 1.578)
AVAILABLE COMMANDS
Built-In Commands
help: Display help about available commands
stacktrace: Display the full stacktrace of the last error.
clear: Clear the shell screen.
quit, exit: Exit the shell.
history: Display or save the history of previously run commands
version: Show version info
script: Read and execute commands from a file.

View File

@@ -2,49 +2,95 @@
To see what Spring Shell has to offer, we can write a trivial shell application that
has a simple command to add two numbers.
=== Writing a Simple Boot Application
IMPORTANT: _Spring Shell_ is based on _Spring Boot_ {spring-boot-version} and
_Spring Framework_ {spring-version} and thus requires _JDK 17_.
Starting with version 2, Spring Shell has been rewritten from the ground up with various
enhancements in mind, one of which is easy integration with Spring Boot.
=== Writing a Simple Shell Application
For the purpose of this tutorial, we create a simple Spring Boot application by
using https://start.spring.io. This minimal application depends only on `spring-boot-starter`
and configures the `spring-boot-maven-plugin` to generate an executable über-jar:
using https://start.spring.io where you can choose _Spring Shell_ dependency.
This minimal application depends only on `spring-boot-starter` and
`spring-shell-starter`.
NOTE: _Spring Shell_ version on `start.spring.io` is usually latest release.
With _maven_ you're expected to have something like:
====
[source, xml]
[source, xml, subs=attributes+]
----
<properties>
<spring-shell.version>{project-version}</spring-shell.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-shell-starter</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-dependencies</artifactId>
<version>${spring-shell.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
----
====
[[using-spring-shell-add-dependency]]
=== Adding a Dependency on Spring Shell
The easiest way to get going with Spring Shell is to depend on the `{starter-artifactId}` artifact.
This comes with everything you need to use Spring Shell and plays nicely with Boot,
configuring only the necessary beans as needed:
With _gradle_ you're expected to have something like:
====
[source, xml, subs=attributes+]
[source, groovy, subs=attributes+]
----
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>{spring-shell-starter}</artifactId>
<version>{project-version}</version>
</dependency>
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.shell:spring-shell-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.shell:spring-shell-dependencies:{project-version}"
}
}
----
====
CAUTION: Given that Spring Shell starts the REPL (Read-Eval-Print-Loop) because this dependency is present,
you need to either skip tests when you build (`-DskipTests`) throughout this tutorial or remove the sample integration test
that was generated by https://start.spring.io. If you do not remove it, the integration test creates
the Spring `ApplicationContext` and, depending on your build tool, stays stuck in the eval loop or crashes with a NPE.
CAUTION: Given that Spring Shell starts the REPL (Read-Eval-Print-Loop) because this
dependency is present, you need to either skip tests when you build (`-DskipTests`)
throughout this tutorial or remove the sample integration test that was generated
by https://start.spring.io. If you do not remove it, the integration test creates
the Spring `ApplicationContext` and, depending on your build tool, stays stuck in
the eval loop or crashes with a NPE.
Once compiled it can be run either in interactive mode:
====
[source, text, subs=attributes+]
----
include::code/getting-started-run-interactive.out[]
----
====
Or in non-interactive mode:
====
[source, text, subs=attributes+]
----
include::code/getting-started-run-noninteractive.out[]
----
====
[[using-spring-shell-your-first-command]]
=== Your First Command
@@ -76,38 +122,24 @@ public class MyCommands {
----
====
[[using-spring-shell-try-application]]
=== Trying the Application
To build the application and run the generated jar, run the following command:
New _add_ command becomes visible to _help_:
====
[source, bash]
[source, text]
----
./mvnw clean install -DskipTests
[...]
java -jar target/demo-0.0.1-SNAPSHOT.jar
My Commands
add: Add two integers together.
----
====
====
[source]
----
shell:>
----
====
A yellow `shell:>` prompt invites you to type commands. Type `add 1 2`, press `ENTER`, and admire the magic:
And you can run it:
====
[source, bash]
[source, text]
----
shell:>add --a 1 --b 2
3
----
====
You should play with the shell (hint: there is a `help` command). When you are done, type `exit` and press `ENTER`.
The rest of this document delves deeper into the whole Spring Shell programming model.

View File

@@ -6,7 +6,7 @@ Eric Bottard; Janne Valkealahti; Jay Bryant; Corneil du Plessis
:experimental: // For kbd: macro
:spring-shell-starter: spring-shell-starter
*{projectVersion}*
*{project-version}*
(C) 2017 - 2022 VMware, Inc.

View File

@@ -12,12 +12,3 @@ the familiar Spring programming model.
Spring Shell includes advanced features (such as parsing, tab completion, colorization of
output, fancy ASCII-art table display, input conversion, and validation), freeing you
to focus on core command logic.
[IMPORTANT]
====
Spring Shell 2.1.x is a major rework to bring the codebase up to date with
existing Spring Boot versions, adding new features and, especially,
making it work with GraalVM which makes command-line applications much
more relevant in a Java space. Moving to a new major version also lets
us clean up the codebase and make some needed breaking changes.
====