Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

major: update to structurizr 2 and spring 3, migrate to java in order to reduce dependencies #5

Merged
merged 1 commit into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
`maven-publish`
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
signing
`java-library`
}

description = "Spring Boot AutoConfigure Support for the Structurizr Client"
Expand All @@ -19,22 +20,26 @@ repositories {
}

dependencies {
api(platform(libs.spring.boot.bom))
kapt(platform(libs.spring.boot.bom))
implementation(platform(libs.spring.boot.bom))
annotationProcessor(platform(libs.spring.boot.bom))

api(libs.structurizr.core)
api(libs.structurizr.client)

api(libs.structurizr.export.c4plantuml)
api(libs.architectureicons)
api(libs.structurizr.export)
implementation(libs.structurizr.client)

// let that be an API dependency in order publish @SpringBootApplication
api("org.springframework.boot:spring-boot-autoconfigure")
runtimeOnly("org.springframework.boot:spring-boot-starter")

kapt("org.springframework.boot:spring-boot-configuration-processor")
compileOnly("org.springframework.boot:spring-boot-configuration-processor")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

testImplementation("org.springframework.boot:spring-boot-test")

testImplementation(platform(libs.junit.jupiter.bom))
testImplementation(libs.architectureicons)
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

java {
Expand Down
9 changes: 6 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
[versions]

structurizr = "1.12.2"
spring-boot = "2.7.1"
structurizr = "2.2.0"
spring-boot = "3.3.1"
junit-jupiter = "5.10.3"


[libraries]

structurizr-core = { module = "com.structurizr:structurizr-core", version.ref = "structurizr" }
structurizr-export = { module = "com.structurizr:structurizr-export", version.ref = "structurizr" }
structurizr-client = { module = "com.structurizr:structurizr-client", version.ref = "structurizr" }

structurizr-export-c4plantuml = { module = "io.cloudflight.structurizr:structurizr-export-c4plantuml", version = "1.0.0" }
architectureicons = { module = "io.cloudflight.architectureicons:architectureicons", version = "0.3.0" }

spring-boot-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" }

junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit-jupiter" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.cloudflight.architecture.structurizr;

import com.structurizr.model.Model;

import javax.annotation.Nonnull;

/**
* Implementors of this interface will be called after all spring components have been initialized but before all implicit
* relationships are created and before all views are generated
*
* @author Klaus Lehner
*/
public interface ModelPostProcessor {
/**
* Do some action on the {@link Model} after the spring context has been initialized
*
* @param model the C4 Model
*/
void postProcess(@Nonnull Model model);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.cloudflight.architecture.structurizr;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;

/**
* Use this class as a starter method for your applications. It wraps SpringApplication and logs an error if there is one
*
* @author Klaus Lehner
*/
public final class SpringStructurizr {

private static final Logger LOG = LoggerFactory.getLogger(SpringStructurizr.class);

private SpringStructurizr() {
}

public static ConfigurableApplicationContext run(Class<?> architectureClass) {
try {
return SpringApplication.run(architectureClass);
} catch (Exception ex) {
LOG.error("Error at creating Structurizr model", ex);
throw ex;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.cloudflight.architecture.structurizr;

import com.structurizr.view.ViewSet;

import javax.annotation.Nonnull;

/**
* Implement this interface if you want to create {@link com.structurizr.view.View}s for your {@link com.structurizr.model.Model}.
* The reason why this is done in a separate method is to ensure that all {@link org.springframework.stereotype.Component}s which
* might manipulate the model during startup phase have been initialized and that the {@link com.structurizr.model.Model} is complete.
*
* @author Klaus Lehner
*/
public interface ViewProvider {
/**
* <p>This method is called automatically after the whole {@link org.springframework.context.ApplicationContext} is refreshed,
* thus the {@link com.structurizr.model.Model} definition should be complete.</p>
*
* <p>Implementations of this method might create one or more {@link com.structurizr.view.View} inside this method</p>
*
* @param viewSet the {@link ViewSet} of the {@link com.structurizr.Workspace} after the whole {@link com.structurizr.model.Model}
* has been initialized
*/
void createViews(@Nonnull ViewSet viewSet);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.cloudflight.architecture.structurizr;

import com.structurizr.Workspace;

import javax.annotation.Nonnull;

public interface WorkspaceExportService {

void exportWorkspace(@Nonnull Workspace workspace);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.cloudflight.architecture.structurizr.autoconfigure;

import com.structurizr.Workspace;
import com.structurizr.api.WorkspaceApiClient;
import com.structurizr.encryption.AesEncryptionStrategy;
import com.structurizr.model.CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy;
import com.structurizr.model.Model;
import io.cloudflight.architecture.structurizr.internal.service.StructurizrService;
import io.cloudflight.architecture.structurizr.internal.service.export.C4PlantUmlExportService;
import io.cloudflight.architecture.structurizr.internal.service.export.StructurizrWorkspaceExportService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
* @author Klaus Lehner
*/
@Configuration
@ComponentScan(basePackageClasses = StructurizrService.class)
@EnableConfigurationProperties(value = StructurizrProperties.class)
class StructurizrAutoConfiguration {

@Bean
StructurizrService structurizrService(Workspace workspace, ApplicationContext applicationContext) {
return new StructurizrService(workspace, applicationContext);
}

@Bean
Model model(Workspace workspace, StructurizrProperties properties) {
if (properties.workspace().isAddImplicitRelationships()) {
workspace.getModel().setImpliedRelationshipsStrategy(
new CreateImpliedRelationshipsUnlessAnyRelationshipExistsStrategy());
}
return workspace.getModel();
}


@Bean
Workspace workspace(StructurizrProperties config) {
return new Workspace(config.workspace().name(), config.workspace().description());
}

@Configuration
@ConditionalOnProperty(value = "structurizr.export.structurizr.enabled", havingValue = "true")
class StructurizrExportConfiguration {
@Bean
WorkspaceApiClient structurizrClient(StructurizrProperties config) {
StructurizrProperties.StructurizrExport structurizr = config.export().structurizr();
WorkspaceApiClient structurizrClient = new WorkspaceApiClient(structurizr.url(), structurizr.key(), structurizr.secret());

// Add client-side encryption if configured
if (structurizr.encryptionPassphrase() != null) {
structurizrClient.setEncryptionStrategy(new AesEncryptionStrategy(structurizr.encryptionPassphrase()));
}
structurizrClient.setWorkspaceArchiveLocation(structurizr.workspaceArchiveLocation());
return structurizrClient;
}

@Bean
StructurizrWorkspaceExportService structurizrWorkspaceExporter(StructurizrProperties properties, WorkspaceApiClient client) {
return new StructurizrWorkspaceExportService(properties, client);
}
}

@Configuration
@ConditionalOnProperty(value = "structurizr.export.c4-plant-uml.enabled", havingValue = "true")
class C4PlantUmlExportConfiguration {
@Bean
C4PlantUmlExportService c4PlantUmlExportService(StructurizrProperties properties) {
return new C4PlantUmlExportService(properties);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.cloudflight.architecture.structurizr.autoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

import java.io.File;

/**
* Configuration properties for Structurizr.
*
* @author Klaus Lehner
*/
@ConfigurationProperties("structurizr")
public record StructurizrProperties(Workspace workspace, Export export) {

public record Export(
StructurizrExport structurizr,
C4PlantUmlExport c4PlantUml
) {
}

/**
* @param enabled if <code>true</code>, the workspace will be exported to a Structurizr server
* @param url the url of the Structurizr server, the default is <code>https://api.structurizr.com</code>
* @param id
* @param key Workspace API key
* @param secret Workspace API secret
* @param encryptionPassphrase Workspace client-side encryption passphrase
* @param workspaceArchiveLocation Directory to use to archive workspaces.
*/
public record StructurizrExport(
@DefaultValue("false")
boolean enabled,
@DefaultValue("https://api.structurizr.com")
String url,
@DefaultValue("0")
Long id,
String key,
String secret,
String encryptionPassphrase,
File workspaceArchiveLocation
) {
}

/**
* @param enabled if <code>true</code>, the Workspace will be exported using the PlantUML exporter
* @param outputDirectory
*/
public record C4PlantUmlExport(
@DefaultValue("false")
boolean enabled,
@DefaultValue("build/c4PlantUml")
File outputDirectory
) {
}

/**
* @param name workspace name
* @param description workspace description
* @param isAddImplicitRelationships Propagates all relationships from children to their parents.
*/
public record Workspace(
String name,
String description,
@DefaultValue("true")
boolean isAddImplicitRelationships
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.cloudflight.architecture.structurizr.internal.service;

import com.structurizr.Workspace;
import io.cloudflight.architecture.structurizr.ModelPostProcessor;
import io.cloudflight.architecture.structurizr.ViewProvider;
import io.cloudflight.architecture.structurizr.WorkspaceExportService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.Order;

/**
* @author Klaus Lehner
*/
@Order(StructurizrService.ORDER)
public class StructurizrService implements CommandLineRunner {

private final Workspace workspace;
private final ApplicationContext applicationContext;

static final int ORDER = 0;

public StructurizrService(Workspace workspace, ApplicationContext applicationContext) {
this.workspace = workspace;
this.applicationContext = applicationContext;
}

@Override
public void run(String... args) throws Exception {
applicationContext.getBeansOfType(ModelPostProcessor.class)
.values()
.stream()
.sorted(AnnotationAwareOrderComparator.INSTANCE)
.forEach(modelPostProcessor -> modelPostProcessor.postProcess(workspace.getModel()));

applicationContext.getBeansOfType(ViewProvider.class)
.values()
.forEach(viewProvider -> viewProvider.createViews(workspace.getViews()));

applicationContext.getBeansOfType(WorkspaceExportService.class)
.values()
.stream()
.sorted(AnnotationAwareOrderComparator.INSTANCE)
.forEach(exporter -> exporter.exportWorkspace(workspace));
}
}
Loading
Loading