AboutController.java
package com.taxonomy.shared.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.boot.info.GitProperties;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* REST API for application version, build info, licenses, notices and SBOMs.
*/
@RestController
@RequestMapping("/api/about")
@Tag(name = "About")
public class AboutController {
private static final Logger log = LoggerFactory.getLogger(AboutController.class);
private static final MediaType MARKDOWN = MediaType.parseMediaType("text/markdown;charset=UTF-8");
private static final MediaType UTF8_TEXT = MediaType.parseMediaType("text/plain;charset=UTF-8");
private final BuildProperties buildProperties;
private final GitProperties gitProperties;
@Autowired(required = false)
public AboutController(BuildProperties buildProperties, GitProperties gitProperties) {
this.buildProperties = buildProperties;
this.gitProperties = gitProperties;
}
public AboutController() {
this.buildProperties = null;
this.gitProperties = null;
}
@GetMapping
@Operation(summary = "Application info", description = "Returns version, build, source and packaged legal-resource information.")
public ResponseEntity<Map<String, Object>> about() {
Map<String, Object> info = new LinkedHashMap<>();
info.put("product", "Taxonomy Architecture Analyzer");
info.put("version", buildProperties != null ? buildProperties.getVersion() : "unknown");
info.put("buildTime", buildProperties != null ? buildProperties.getTime() : null);
info.put("commit", gitProperties != null ? gitProperties.getShortCommitId() : "unknown");
info.put("commitTime", gitProperties != null ? gitProperties.getCommitTime() : null);
info.put("branch", gitProperties != null ? gitProperties.getBranch() : "unknown");
info.put("license", "MIT");
info.put("copyright", "Copyright 2026 Carsten Hammer");
info.put("sourceUrl", "https://github.com/carstenartur/Taxonomy");
info.put("apiDocsUrl", "/swagger-ui.html");
info.put("projectLicenseUrl", "/api/about/license");
info.put("noticeUrl", "/api/about/notice");
info.put("thirdPartyNoticesUrl", "/api/about/third-party");
info.put("runtimeThirdPartyLicensesUrl", "/api/about/runtime-licenses");
info.put("sbomJsonUrl", "/api/about/sbom.json");
info.put("sbomXmlUrl", "/api/about/sbom.xml");
return ResponseEntity.ok(info);
}
@GetMapping("/license")
@Operation(summary = "Project license", description = "Returns the packaged Taxonomy MIT license.")
public ResponseEntity<String> license() {
return classpathText("static/LICENSE", UTF8_TEXT);
}
@GetMapping("/notice")
@Operation(summary = "Product notice", description = "Returns the packaged product NOTICE file.")
public ResponseEntity<String> notice() {
return classpathText("static/NOTICE", UTF8_TEXT);
}
@GetMapping("/third-party")
@Operation(summary = "Curated third-party notices", description = "Returns curated notices for browser assets, models, containers and special distribution terms.")
public ResponseEntity<String> thirdParty() {
return classpathText("static/THIRD-PARTY-NOTICES.md", MARKDOWN);
}
@GetMapping("/runtime-licenses")
@Operation(summary = "Generated runtime dependency licenses", description = "Returns the build-generated report for packaged Maven runtime dependencies.")
public ResponseEntity<String> runtimeLicenses() {
return classpathText("THIRD-PARTY-RUNTIME.txt", UTF8_TEXT);
}
@GetMapping("/sbom.json")
@Operation(summary = "CycloneDX SBOM as JSON", description = "Returns the build-specific packaged CycloneDX software bill of materials.")
public ResponseEntity<String> sbomJson() {
return classpathText("static/taxonomy-sbom.json", MediaType.APPLICATION_JSON);
}
@GetMapping("/sbom.xml")
@Operation(summary = "CycloneDX SBOM as XML", description = "Returns the build-specific packaged CycloneDX software bill of materials.")
public ResponseEntity<String> sbomXml() {
return classpathText("static/taxonomy-sbom.xml", MediaType.APPLICATION_XML);
}
private ResponseEntity<String> classpathText(String path, MediaType contentType) {
ClassPathResource resource = new ClassPathResource(path);
if (!resource.exists()) {
log.warn("About resource not found on classpath: {}", path);
return ResponseEntity.notFound().build();
}
try (InputStream input = resource.getInputStream()) {
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
return ResponseEntity.ok().contentType(contentType).body(content);
} catch (IOException e) {
log.warn("Could not read about resource {}: {}", path, e.getMessage());
return ResponseEntity.internalServerError().contentType(UTF8_TEXT)
.body("Packaged legal resource could not be read.");
}
}
}