Table of Contents

API documentation (Swagger)

indexmenu_n_60

We use swagger specification for RESTful API documentation with tools:

Difference between dynamic and static documentation:
  • Dynamic documentation is exposed by Swagger UI from rest controllers with @Api annotation. Documentation is available on root backend server url e.g.<server>/api - documented endpoints can be tested with demo credentials directly from Swagger UI. Documentation is splitted by modules. Raw Swagger specification in json format is available on url e.g. <server>/api/doc?group=core. Documentation always contains authentication endpoint (security information).
  • Static documentation is generated from raw swagger specification (e.g. <server>/api/doc?group=core) and static content (see next chapter with static documentation folder structure). Documentation is splitted by modules. Generated output html can be found in <module>/target/asciidoc/html/index.html. This documentation can be exposed as api reference and is exposed directly in application, which is built under release profile on urls with convention <server>/webjars/<module>/<version>/doc/index.html.

Configuration

Parent project contains basic settings for module documentation. When a new module is added, some steps have to be done. Complete configuration can be found in example module.

Java

Module properties

Use PropertyModuleDescriptor generalization for module descriptor definition and prepare module-example.properties.

/**
 * Example module descriptor
 */
@Component
@PropertySource("classpath:module-" + ExampleModuleDescriptor.MODULE_ID + ".properties")
@ConfigurationProperties(prefix = "module." + ExampleModuleDescriptor.MODULE_ID + ".build", ignoreUnknownFields = true, ignoreInvalidFields = true)
public class ExampleModuleDescriptor extends PropertyModuleDescriptor {
 
	public static final String MODULE_ID = "example";
 
	@Override
	public String getId() {
		return MODULE_ID;
	}
 
        /**
	 * Enables links to swagger documentation
	 */
	@Override
	public boolean isDocumentationAvailable() {
		return true;
	}
}

Swagger endpoint

/**
 * Example module swagger configuration
 */
@Configuration
@ConditionalOnProperty(prefix = "springfox.documentation.swagger", name = "enabled", matchIfMissing = true)
public class ExampleSwaggerConfig extends AbstractSwaggerConfig {
 
	@Autowired private ExampleModuleDescriptor moduleDescriptor;
 
	@Override
	protected ModuleDescriptor getModuleDescriptor() {
		return moduleDescriptor;
	}
 
	@Bean
	public Docket exampleApi() {
		return api("eu.bcvsolutions.idm.example");
	}
}

springdoc version (13.1.0+)

/**
 * Example module swagger configuration
 */
@Configuration
@ConditionalOnProperty(prefix = "springdoc.swagger-ui", name = "enabled", matchIfMissing = true)
public class ExampleSwaggerConfig extends AbstractSwaggerConfig {
 
	@Autowired private ExampleModuleDescriptor moduleDescriptor;
 
	@Override
	protected ModuleDescriptor getModuleDescriptor() {
		return moduleDescriptor;
	}
 
	@Bean
	public GroupedOpenApi exampleApi() {
		return api("eu.bcvsolutions.idm.example.rest");
	}
}

Rest controller

Add swagger annotations to controllers.

/**
 * Ping pong example controller
 */
@RestController
@RequestMapping(value = BaseController.BASE_PATH + "/examples", produces = BaseController.APPLICATION_HAL_JSON_VALUE)
@Api(value = "Examples", description = "Example operations", tags = { "Examples" })
public class ExampleController {
 
	@ResponseBody
	@RequestMapping(method = RequestMethod.GET)
	@ApiOperation(
			value = "Ping - Pong operation", 
			notes= "Returns message with additional informations",
			nickname = "ping", 
			tags={ "Examples" }, 
			response = Pong.class, 
			authorizations = {
				@Authorization(SwaggerConfig.AUTHENTICATION_BASIC),
				@Authorization(SwaggerConfig.AUTHENTICATION_CIDMST)
			})
	public ResponseEntity<?> ping(
			@ApiParam(value = "In / out message", example = "hello", defaultValue = "hello") 
			@RequestParam(required = false, defaultValue = "hello") String message
			) {
		return new ResponseEntity<>(new Pong(message), HttpStatus.OK); 
	}
}

For springdoc version (13.1.0+) add new swagger annotations to controllers.

/**
 * Ping pong example controller
 */
@RestController
@Enabled(ExampleModuleDescriptor.MODULE_ID)
@RequestMapping(value = BaseController.BASE_PATH + "/examples")
@Tag(name = ExampleController.TAG, description = "Example operations")
 
public class ExampleController {
 
	protected static final String TAG = "Examples";
	@Autowired private ExampleService service;
 
	@ResponseBody
	@RequestMapping(method = RequestMethod.GET, path = "/ping")
	@Operation(
		summary = "Ping - Pong operation", 
		description= "Returns message with additional informations",
		operationId = "ping",
                tags={ ExampleController.TAG }
                responses = @ApiResponse(
                    responseCode = "200",
                    content = {
                            @Content(
                                    mediaType = BaseController.APPLICATION_HAL_JSON_VALUE,
                                    schema = @Schema(
                                            implementation = Pong.class
                                    )
                            )
                    }
            ))
    @SecurityRequirements({
        @SecurityRequirement(name = SwaggerConfig.AUTHENTICATION_BASIC),
        @SecurityRequirement(name = SwaggerConfig.AUTHENTICATION_CIDMST)
    })
	public ResponseEntity<Pong> ping(
			@Parameter(description = "In / out message", example = "hello")
			@RequestParam(required = false, defaultValue = "hello") String message
			) {
		return new ResponseEntity<>(service.ping(message), HttpStatus.OK); 
	}
}

Model

Add swagger annotations to dtos.

/**
 * Example ping - pong response dto
 */
@ApiModel(description = "Ping - Pong response")
public class Pong implements BaseDto {
 
	private static final long serialVersionUID = 1L;
	//
	@ApiModelProperty(required = true, notes = "Unique pong identifier")
	private UUID id;
	@ApiModelProperty(notes = "Ping - Pong response message")
	private String message;
	@ApiModelProperty(required = true, notes = "Creation time")
	private DateTime created;
 
	// ... getters, setters
}

For springdoc version (13.1.0+) add new swagger annotations to dtos.

/**
 * Example ping - pong response dto
 */
@Schema(description = "Ping - Pong response")
public class Pong implements BaseDto {
 
	private static final long serialVersionUID = 1L;
	//
	@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique pong identifier")
	private UUID id;
	@Schema(description = "Ping - Pong response message")
	private String message;
	@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Creation time")
	private ZonedDateTime created;
 
	// ... getters, setters
}

Test

Create integration test:

/**
 * Static swagger generation to sources - will be used as input for swagger2Markup build
 */
public class Swagger2MarkupTest extends AbstractSwaggerTest {
 
	@Test
	public void testConvertSwagger() throws Exception {
		super.convertSwagger(ExampleModuleDescriptor.MODULE_ID);
	}
 
}

Static documentation folder structure

We are using asciidoctor maven plugin for static documentation (see maven chapter). Maven plugins prepare all generated artifacts, but requires some static artifacts with static / written documentation in structure in module sources folder:

All files have to be written in asciidoc format. Read more about extensions in swagger2markup documentation.

Example extension

Add security information about demo identity credentials. Create file <module>/src/docs/asciidoc/extensions/security/document-begin-text.adoc with content:

== Demo credentials
 
admin / admin

This content will be automatically included to static documentation to the security section. Read more about available extension points.

Maven

Documentation is generated under release profile. Add profile to module pom.xml:

<profile>
	<id>release</id>
	<build>
		<plugins>
			<!-- First, use the swagger2markup plugin to generate asciidoc -->
			<plugin>
				<groupId>io.github.swagger2markup</groupId>
				<artifactId>swagger2markup-maven-plugin</artifactId>
				<version>${swagger2markup.version}</version>
 
				<configuration>
					<swaggerInput>${swagger.input}</swaggerInput>
					<outputDir>${generated.asciidoc.directory}</outputDir>
					<config>
						<swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage>
						<swagger2markup.outputLanguage>EN</swagger2markup.outputLanguage>
						<swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy>
						<swagger2markup.generatedExamplesEnabled>false</swagger2markup.generatedExamplesEnabled>
 
						<swagger2markup.extensions.dynamicOverview.contentPath>${asciidoctor.input.extensions.directory}/overview</swagger2markup.extensions.dynamicOverview.contentPath>
						<swagger2markup.extensions.dynamicDefinitions.contentPath>${asciidoctor.input.extensions.directory}/definitions</swagger2markup.extensions.dynamicDefinitions.contentPath>
						<swagger2markup.extensions.dynamicPaths.contentPath>${asciidoctor.input.extensions.directory}/paths</swagger2markup.extensions.dynamicPaths.contentPath>
						<swagger2markup.extensions.dynamicSecurity.contentPath>${asciidoctor.input.extensions.directory}/security/</swagger2markup.extensions.dynamicSecurity.contentPath>
 
						<swagger2markup.extensions.springRestDocs.snippetBaseUri>${swagger.snippetOutput.dir}</swagger2markup.extensions.springRestDocs.snippetBaseUri>
						<swagger2markup.extensions.springRestDocs.defaultSnippets>true</swagger2markup.extensions.springRestDocs.defaultSnippets>
					</config>
				</configuration>
				<executions>
					<execution>
						<phase>test</phase>
						<goals>
							<goal>convertSwagger2markup</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
 
			<!--
				Run the generated asciidoc through Asciidoctor to generate other
				documentation types, such as PDFs or HTML5
			-->
			<plugin>
				<groupId>org.asciidoctor</groupId>
				<artifactId>asciidoctor-maven-plugin</artifactId>
				<version>1.5.3</version>
				<!-- Configure generic document generation settings -->
				<configuration>
					<sourceDirectory>${asciidoctor.input.directory}</sourceDirectory>
					<sourceDocumentName>index.adoc</sourceDocumentName>
					<attributes>
						<doctype>book</doctype>
						<toc>left</toc>
						<toclevels>2</toclevels> <!-- Resources by tag names in menu only -->
						<numbered />
						<hardbreaks />
						<sectlinks />
						<sectanchors />
						<generated>${generated.asciidoc.directory}</generated>
					</attributes>
				</configuration>
				<!--
					Since each execution can only handle one backend, run separate
					executions for each desired output type
				-->
				<executions>
					<execution>
						<id>output-html</id>
						<phase>test</phase>
						<goals>
							<goal>process-asciidoc</goal>
						</goals>
						<configuration>
							<backend>html5</backend>
							<!-- static documentation will be available as webjars -->
							<!-- e.g. http://localhost:8080/idm/webjars/core/7.3.0/doc/index.html -->
							<outputDirectory>${asciidoctor.html.output.directory.prefix}/core/${project.version}/doc</outputDirectory>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</profile>

All maven properties are preconfigured in parent pom.xml:

<swagger.version>2.7.0</swagger.version>
<swagger2markup.version>1.3.1</swagger2markup.version>
 
<!-- 
	properties are used ind test and doc profile,
	test: swagger.json input is generated
	doc: swagger2markup and asciidoctor plugins require them
 -->
<asciidoctor.input.directory>${project.basedir}/src/docs/asciidoc</asciidoctor.input.directory>
<asciidoctor.input.extensions.directory>${asciidoctor.input.directory}/extensions</asciidoctor.input.extensions.directory>
<swagger.output.dir>${project.build.directory}/swagger</swagger.output.dir>
<swagger.output.filename>swagger.json</swagger.output.filename>
<swagger.input>${swagger.output.dir}/${swagger.output.filename}</swagger.input>
<swagger.snippetOutput.dir>${project.build.directory}/asciidoc/snippets</swagger.snippetOutput.dir>
<generated.asciidoc.directory>${project.build.directory}/asciidoc/generated</generated.asciidoc.directory>
<!-- static documentation will be available as webjars -->
<!-- e.g. http://localhost:8080/idm/webjars/core/7.3.0/doc/index.html -->
<asciidoctor.html.output.directory.prefix>${project.build.directory}/classes/META-INF/resources/webjars</asciidoctor.html.output.directory.prefix>
<asciidoctor.html.output.directory>${asciidoctor.html.output.directory.prefix}/${project.artifactId}/${project.version}/doc</asciidoctor.html.output.director

Conventions

in springdoc version (13.1.0+)

Aggregator

When module aggregator is built under release profile, then static html and javadoc are packed into archive /target/<version>-doc.zip (tar.gz, tar.bz2).

cd aggregator/
mvn package -Prelease -DdocumentationOnly=true
Which documentation are packed into documentation archive is configured in /src/assembly/doc.xml descriptor. When new CzechIdM product module is created, don't forget to add new module here.

Tips

Use IdmIdentityController as inspiration.

Export swagger.json by running single test:

mvn clean package -DskipTests
mvn surefire:test -Dtest=Swagger2MarkupTest -Prelease

Generate static documentation only (swagger.json has to be exported - see previous tip). Generated html will be available in project's folder /target/classes/META-INF/resources/webjars/<module>/<version>/doc/index.html:

mvn package -DskipTests -Prelease

Static html documentation will be available from url (application's war has to be build under release profile):

<server>/webjars/<module>/<version>/doc/index.html
// e.g.
http://localhost:8080/idm/webjars/core/7.3.0-rc.4-SNAPSHOT/doc/index.html

Implementation details