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

Allow customization of the diagram exporter #382

Closed
Closed
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ Generatr container. So
is needed to expose the container's port 8080 to the host (web browser). In the example above, the
`-p 8080:8080` argument tells Docker to bind the local machine / host's port 8080 to the container's port 8080.

### Customize the diagram exporter

To customize the exported diagrams the paramater `--exporter-type` or `-exp` can be used.

Currently supported exporters:
* `c4` uses the `C4PlantUMLExporter` (default)
* `structurizr` uses the `StructurizrPlantUMLExporter`

## Customizing the generated website

The site generator use the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ class GenerateSiteCommand : Subcommand(
ArgType.String, "exclude-branches", "ex",
"Comma-separated list of branches to exclude from the generated site"
).default("")

private val exporterType by option(
ArgType.String, "exporter-type", "exp",
"Set diagram exporter type. (C4 and Structurizr are supported)."
).default(value = "C4")
override fun execute() {
val siteDir = File(outputDir).apply { mkdirs() }
val gitUrl = gitUrl
Expand Down Expand Up @@ -114,28 +117,30 @@ class GenerateSiteCommand : Subcommand(
clonedRepository.checkoutBranch(branch)

val workspace = createStructurizrWorkspace(workspaceFileInRepo)
generateDiagrams(workspace, File(siteDir, branch))
generateDiagrams(workspace, File(siteDir, branch), exporterType)
generateSite(
version,
workspace,
assetsDir?.let { File(cloneDir, it) },
siteDir,
branchesToGenerate,
branch
branch,
exporterType
)
}
}

private fun generateSiteForModel(siteDir: File) {
val workspace = createStructurizrWorkspace(File(workspaceFile))
generateDiagrams(workspace, File(siteDir, defaultBranch))
generateDiagrams(workspace, File(siteDir, defaultBranch), exporterType)
generateSite(
version,
workspace,
assetsDir?.let { File(it) },
siteDir,
listOf(defaultBranch),
defaultBranch
defaultBranch,
exporterType
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class ServeCommand : Subcommand("serve", "Start a development server") {
private val port by option(
ArgType.Int, "port", "p", "Port the site is served on"
).default(8080)
private val exporterType by option(
ArgType.String, "exporter-type", "exp",
"Set diagram exporter type. (C4 and Structurizr are supported)."
).default(value = "C4")

private val eventSockets = mutableListOf<EventSocket>()
private val eventSocketsLock = Any()
Expand Down Expand Up @@ -80,7 +84,7 @@ class ServeCommand : Subcommand("serve", "Start a development server") {
println("Copying assets...")
copySiteWideAssets(File(siteDir))
println("Generating diagrams...")
generateDiagrams(workspace, exportDir)
generateDiagrams(workspace, exportDir, exporterType)
println("Generating site...")
generateSite(
"0.0.0",
Expand All @@ -89,6 +93,7 @@ class ServeCommand : Subcommand("serve", "Start a development server") {
File(siteDir),
listOf(branch),
branch,
exporterType,
serving = true
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package nl.avisi.structurizr.site.generatr.site
import com.structurizr.Workspace
import com.structurizr.export.Diagram
import com.structurizr.export.IndentingWriter
import com.structurizr.export.plantuml.AbstractPlantUMLExporter
import com.structurizr.export.plantuml.C4PlantUMLExporter
import com.structurizr.model.Container
import com.structurizr.model.Element
Expand All @@ -17,7 +18,7 @@ class C4PlantUmlExporterWithElementLinks(
companion object {
const val TEMP_URI = "https://will-be-changed-to-relative/"

fun C4PlantUMLExporter.export(view: View): Diagram = when (view) {
fun AbstractPlantUMLExporter.export(view: View): Diagram = when (view) {
is CustomView -> export(view)
is SystemLandscapeView -> export(view)
is SystemContextView -> export(view)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@ package nl.avisi.structurizr.site.generatr.site

import com.structurizr.Workspace
import com.structurizr.export.Diagram
import com.structurizr.export.plantuml.C4PlantUMLExporter
import com.structurizr.export.plantuml.PlantUMLDiagram
import com.structurizr.view.ModelView
import com.structurizr.view.View
import net.sourceforge.plantuml.FileFormat
import net.sourceforge.plantuml.FileFormatOption
import net.sourceforge.plantuml.SourceStringReader
import nl.avisi.structurizr.site.generatr.includedSoftwareSystems
import nl.avisi.structurizr.site.generatr.site.C4PlantUmlExporterWithElementLinks.Companion.export
import nl.avisi.structurizr.site.generatr.site.factory.PlantUMLExporterFactory
import java.io.ByteArrayOutputStream
import java.io.File
import java.net.URL
import java.util.concurrent.ConcurrentHashMap

fun generateDiagrams(workspace: Workspace, exportDir: File) {
val exporterFactory = PlantUMLExporterFactory()

fun generateDiagrams(workspace: Workspace, exportDir: File, exporterType: String) {
val pumlDir = pumlDir(exportDir)
val svgDir = svgDir(exportDir)
val pngDir = pngDir(exportDir)

val plantUMLDiagrams = generatePlantUMLDiagrams(workspace)
val plantUMLDiagrams = generatePlantUMLDiagrams(workspace, exporterType)

plantUMLDiagrams.parallelStream()
.forEach { diagram ->
Expand All @@ -41,9 +42,10 @@ fun generateDiagramWithElementLinks(
workspace: Workspace,
view: View,
url: String,
exporterType: String,
diagramCache: ConcurrentHashMap<String, String>
): String {
val diagram = generatePlantUMLDiagramWithElementLinks(workspace, view, url)
val diagram = generatePlantUMLDiagramWithElementLinks(workspace, view, url, exporterType)

val name = "${diagram.key}-${view.key}"
return diagramCache.getOrPut(name) {
Expand All @@ -55,8 +57,8 @@ fun generateDiagramWithElementLinks(
}
}

private fun generatePlantUMLDiagrams(workspace: Workspace): Collection<Diagram> {
val plantUMLExporter = C4PlantUMLExporter()
private fun generatePlantUMLDiagrams(workspace: Workspace, exporterType: String): Collection<Diagram> {
val plantUMLExporter = exporterFactory.makeExporter(exporterType)

return plantUMLExporter.export(workspace)
}
Expand All @@ -83,8 +85,8 @@ private fun saveAsPng(diagram: Diagram, pngDir: File) {
}
}

private fun generatePlantUMLDiagramWithElementLinks(workspace: Workspace, view: View, url: String): Diagram {
val plantUMLExporter = C4PlantUmlExporterWithElementLinks(workspace, url)
private fun generatePlantUMLDiagramWithElementLinks(workspace: Workspace, view: View, url: String, exporterType: String): Diagram {
val plantUMLExporter = exporterFactory.makeExporterWithLinks(exporterType, workspace, url)

if (workspace.views.configuration.properties.containsKey("generatr.svglink.target")) {
plantUMLExporter.addSkinParam(
Expand All @@ -96,6 +98,7 @@ private fun generatePlantUMLDiagramWithElementLinks(workspace: Workspace, view:
return plantUMLExporter.export(view)
}


private fun pumlDir(exportDir: File) = File(exportDir, "puml").apply { mkdirs() }
private fun svgDir(exportDir: File) = File(exportDir, "svg").apply { mkdirs() }
private fun pngDir(exportDir: File) = File(exportDir, "png").apply { mkdirs() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,13 @@ fun generateSite(
exportDir: File,
branches: List<String>,
currentBranch: String,
exporterType: String,
serving: Boolean = false
) {
val generatorContext = GeneratorContext(version, workspace, branches, currentBranch, serving) { key, url ->
val diagramCache = ConcurrentHashMap<String, String>()
workspace.views.views.singleOrNull { view -> view.key == key }
?.let { generateDiagramWithElementLinks(workspace, it, url, diagramCache) }
?.let { generateDiagramWithElementLinks(workspace, it, url, exporterType, diagramCache) }
}

val branchDir = File(exportDir, currentBranch)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package nl.avisi.structurizr.site.generatr.site

import com.structurizr.Workspace
import com.structurizr.export.Diagram
import com.structurizr.export.IndentingWriter
import com.structurizr.export.plantuml.AbstractPlantUMLExporter
import com.structurizr.export.plantuml.StructurizrPlantUMLExporter
import com.structurizr.model.Container
import com.structurizr.model.Element
import com.structurizr.model.SoftwareSystem
import com.structurizr.view.*
import nl.avisi.structurizr.site.generatr.*

class StructurizrPlantUmlExporterWithElementLinks(
private val workspace: Workspace,
private val url: String
) : StructurizrPlantUMLExporter() {
companion object {
const val TEMP_URI = "https://will-be-changed-to-relative/"

fun AbstractPlantUMLExporter.export(view: View): Diagram = when (view) {
is CustomView -> export(view)
is SystemLandscapeView -> export(view)
is SystemContextView -> export(view)
is ContainerView -> export(view)
is ComponentView -> export(view)
is DynamicView -> export(view)
is DeploymentView -> export(view)
else -> throw IllegalStateException("View ${view.name} has a non-exportable type")
}
}

override fun writeHeader(view: ModelView, writer: IndentingWriter) {
super.writeHeader(view, writer)
writer.writeLine("skinparam svgDimensionStyle false")
writer.writeLine("skinparam preserveAspectRatio meet")
}

override fun writeElement(view: ModelView?, element: Element?, writer: IndentingWriter?) {
val url = when {
needsLinkToSoftwareSystem(element, view) -> getUrlToSoftwareSystem(element)
needsLinkToContainerViews(element, view) -> getUrlToContainerViews(element)
needsLinkToComponentViews(element, view) -> getUrlToComponentViews(element)
else -> null
}

if (url != null)
writeElementWithCustomUrl(element, url, view, writer)
else
super.writeElement(view, element, writer)
}

private fun needsLinkToSoftwareSystem(element: Element?, view: ModelView?) =
element is SoftwareSystem && workspace.includedSoftwareSystems.contains(element) && element != view?.softwareSystem

private fun getUrlToSoftwareSystem(element: Element?): String {
val path = "/${element?.name?.normalize()}/context/".asUrlToDirectory(url)
return "$TEMP_URI$path"
}

private fun needsLinkToContainerViews(element: Element?, view: ModelView?) =
element is SoftwareSystem && workspace.includedSoftwareSystems.contains(element) && element == view?.softwareSystem && element.hasContainers

private fun getUrlToContainerViews(element: Element?): String {
val path = "/${element?.name?.normalize()}/container/".asUrlToDirectory(url)
return "$TEMP_URI$path"
}

private fun needsLinkToComponentViews(element: Element?, view: ModelView?) =
element is Container && element.hasComponents && view !is ComponentView

private fun getUrlToComponentViews(element: Element?): String {
val path = "/${element?.parent?.name?.normalize()}/component/".asUrlToDirectory(url)
return "$TEMP_URI$path"
}

private fun writeElementWithCustomUrl(element: Element?, url: String?, view: ModelView?, writer: IndentingWriter?) {
element?.url = url
writeModifiedElement(view, element, writer)
element?.url = null
}

private fun writeModifiedElement(
view: ModelView?,
element: Element?,
writer: IndentingWriter?
) = IndentingWriter().let {
super.writeElement(view, element, it)
it.toString()
.replace(TEMP_URI, "")
.split(System.lineSeparator())
.forEach { line -> writer?.writeLine(line) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package nl.avisi.structurizr.site.generatr.site.factory

import com.structurizr.Workspace
import com.structurizr.export.plantuml.AbstractPlantUMLExporter
import com.structurizr.export.plantuml.C4PlantUMLExporter
import com.structurizr.export.plantuml.StructurizrPlantUMLExporter
import nl.avisi.structurizr.site.generatr.site.C4PlantUmlExporterWithElementLinks
import nl.avisi.structurizr.site.generatr.site.StructurizrPlantUmlExporterWithElementLinks
import java.util.*

class PlantUMLExporterFactory {
fun makeExporter(type: String): AbstractPlantUMLExporter {
return when(type.lowercase(Locale.getDefault())) {
"c4" -> C4PlantUMLExporter()
"structurizr" -> StructurizrPlantUMLExporter()
else -> throw Exception("unknown diagram exporter type")
}
}

fun makeExporterWithLinks(type: String, workspace: Workspace, url: String): AbstractPlantUMLExporter {
return when(type.lowercase(Locale.getDefault())) {
"c4" -> C4PlantUmlExporterWithElementLinks(workspace,url)
"structurizr" -> StructurizrPlantUmlExporterWithElementLinks(workspace, url)
else -> throw Exception("unknown diagram exporter type")
}
}
}