It was one small change from 1.3.x to 1.4.0, but unfortunately it may be a large change for you ()depending how many Cypher queries you already have): to harmonize relation names with jQAssistant main distribution.

In jQAssistant main distribution all relation names are in singular. The Kontext E plugins often used plural names. It felt natural for me because of the 1:N nature of the relations, and I didn’t notice until Dirk told me.

After some considerations I decided that a unique style of naming in the jQAssistant world weights more than the inconvenience of changing existing queries. So I changed the plural names to singular as described in the following sections.

:FindBugs:BugInstance

  • CLASS -> HAS_CLASS
  • METHODS -> HAS_METHOD
  • FIELDS -> HAS_FIELD

:FindBugs:SourceLineContainer

  • SOURCELINE -> HAS_SOURCELINE

:Asciidoc:List

  • HAS_ITEMS -> HAS_ITEM

:Asciidoc:Table

  • COLUMNS -> HAS_COLUMN
  • HEADER -> HAS_HEADER
  • BODY -> HAS_BODY
  • FOOTER -> HAS_FOOTER

:Asciidoc:Row

  • CONTAINS_CELLS -> CONTAINS_CELL

:Checkstyle:File

  • CHECKSTYLE_ERRORS -> CHECKSTYLE_ERROR

:Checkstyle:Report

  • CHECKSTYLE_FILES -> CHECKSTYLE_FILE

:Jacoco:Class

  • HAS_METHODS -> HAS_METHOD

:Jacoco:Method

  • HAS_COUNTERS -> HAS_COUNTER

:Jacoco:Package

  • HAS_CLASSES -> HAS_CLASS

:Jacoco:Report

  • HAS_PACKAGES -> HAS_PACKAGE

:Plaintext:Directory

  • HAS_FILES -> HAS_FILE
  • HAS_DIRECTORIES -> HAS_DIRECTORY

:Plaintext:File

  • HAS_LINES -> HAS_LINE

:PlantUml:Diagram

  • CONTAINS_GROUPS -> CONTAINS_GROUP

:PlantUml:File

  • CONTAINS_DIAGRAMS -> CONTAINS_DIAGRAM

:PlantUml:Group

  • HAS_CHILD_GROUPS -> HAS_CHILD_GROUP
  • HAS_LEAFS -> HAS_LEAF

:Pmd:File

  • HAS_VIOLATIONS -> HAS_VIOLATION

:Pmd:Report

  • HAS_FILES -> HAS_FILE

Ultrashort TL;DR:

Add this line into your build.gradle:

1
apply from: 'https://raw.githubusercontent.com/kontext-e/jqa-gradle/master/jqa.gradle'

Now you can do

1
./gradlew clean check jqa 

in your project.

The following longer version explains why and how it works.

jQAssistant comes with an excellent Maven support. But does it mean that you can’t use it with other build tools? Of course you can use it also with Gradle. Or Ant. Or whatever you are using. In this post I’ll show you how.

At the Get Started page you find only the options “Command Line” and “Maven”. Well, yes, we could use the “Run Process” capability of the build tool to start the jqassistant.cmd or jqassistant.sh. But there is also the artifact jqassistant-commandline available at Maven Central. Using that one I’ll show you the example of a Gradle integration.

Given you have already a build.gradle file, first you add some properties to make version handling easier:

1
2
3
project.ext["jqaversion"] = "1.3.0"
project.ext["jqapluginversion"] = "1.3"
project.ext["kejqapluginversion"] = "1.3.3"

The “jqaversion” is for the jQAssistant Core Framework. With a high probability you also want to use some plugins, e.g. for scanning Java class files. For the plugins is the “jqapluginversion”. And in this example let’s use also a plugin from an external contributor, so we need also the “kejqapluginversion”.

It is not really necessary, but for the reason of Separations of Concerns, I always like to define a separate “configuration” for jQAssistant dependencies:

1
2
3
configurations {
  jqa
}

Now let’s use this configuration to declare the dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
dependencies {
  jqa("com.buschmais.jqassistant:jqassistant-commandline:${project.jqaversion}") {
    // because jQA 1.3 comes with Neo4j 2 and 3 support, there would be a classpath conflict
    exclude module: 'neo4j'
  }

  // list here all the plugins you want to use
  jqa("com.buschmais.jqassistant.plugin:java:${project.jqapluginversion}")
  jqa("com.buschmais.jqassistant.plugin:junit:${project.jqapluginversion}")

  // and plugins from other contributors
  jqa("de.kontext-e.jqassistant.plugin:jqassistant.plugin.git:${project.kejqapluginversion}")
}

And everything is set up to scan the project. To do so let’s define a new task:

1
2
3
4
5
6
7
8
9
task(jqascan, type: JavaExec) {
    main = 'com.buschmais.jqassistant.commandline.Main'
    classpath = configurations.jqa
    args 'scan'
    args '-f'

    args 'java:classpath::build/classes/main'
    args 'java:classpath::build/classes/test'
}

As you see in the task a Java program gets executed. We have to give the classpath because we defined a separate configuration, the class containing the main method and the parameters as described in the jQAssistant documentation

Finally it’s time to scan your project:

1
./gradlew clean check jqascan

The same way works defining a task for applying concepts and checking constraints:

1
2
3
4
5
task(jqaanalyze, type: JavaExec) {
  classpath = configurations.jqa
  main = 'com.buschmais.jqassistant.commandline.Main'
  args 'analyze'
}

Don’t forget to put some rules in your projects jqassistant/rules folder as described in the Get Started and jQAssistant documentation

and try

1
./gradlew jqaanalyze

Maybe you also want to start the server to explore your application. Let’s also define a task for this:

1
2
3
4
5
6
7
8
task(jqs, type: JavaExec) {
  classpath = configurations.jqa

  main = 'com.buschmais.jqassistant.commandline.Main'
  args 'server'

  standardInput = System.in
}

The jQA command line runs the server and prints “ServerTask - Press to finish." as the last line. To be able to Press we need to declare the

1
standardInput = System.in

as the last line in this task.

Basically that’s it. You may find it useful to define more tasks for the other command line commands like available-rules, available-rules, effective-rules and so on. Especially cleanup is useful. Because resetting the store may take a while, I found it useful to simply delete the store and report folders with this task:

1
2
3
4
task jqaclean(type: Delete) {
    delete 'jqassistant/report'
    delete 'jqassistant/store'
}

and execute this task also in the standard clean task:

1
clean.dependsOn jqaclean

But wait… what if I have a multi-module project? Do I have to list every of my subprojects here? And didn’t I also promise to use a third party plugin? At least the Git plugin is specified as dependency!

Ok, ok, here is a task definition for scan which is slightly longer:

1
2
3
4
5
6
7
8
9
10
11
12
13
task(jqascan, dependsOn: ['jqaclean'], type: JavaExec) {
  classpath = configurations.jqa
  main = 'com.buschmais.jqassistant.commandline.Main'
  args 'scan'
  args '-f'

  rootProject.subprojects {
    args 'java:classpath::'+it.name+'/build/classes/main'
    args 'java:classpath::'+it.name+'/build/classes/test'
  }

  args '.git'
}

This scans all subprojects and also the Git repository. If you may ask yourself here why you should scan the Git repo and what it has to do with a “Java Quality Assistant”, you find the answer in this excellent blog post

Here is the complete code with some additional convenciece tasks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
project.ext["jqaversion"] = "1.3.0"
project.ext["jqapluginversion"] = "1.3"
project.ext["kejqapluginversion"] = "1.3.3"

configurations {
    jqa
}

dependencies {
    jqa("com.buschmais.jqassistant:jqassistant-commandline:${project.jqaversion}") {
        // because jQA 1.3 comes with Neo4j 2 and 3 support, there would be a classpath conflict
        exclude module: 'neo4j'
    }

    // list here all the plugins you want to use
    jqa("com.buschmais.jqassistant.plugin:java:${project.jqapluginversion}")
    jqa("com.buschmais.jqassistant.plugin:junit:${project.jqapluginversion}")

    // and plugins from other contributors
    jqa("de.kontext-e.jqassistant.plugin:jqassistant.plugin.git:${project.kejqapluginversion}")
}

task jqaclean(type: Delete) {
    delete 'jqassistant/report'
    delete 'jqassistant/store'
}

task(jqascan, dependsOn: 'jqaclean', type: JavaExec) {
    main = 'com.buschmais.jqassistant.commandline.Main'
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'scan'
    args '-f'

    args 'java:classpath::build/classes/main'
    args 'java:classpath::build/classes/test'

    /* in a multi subprojects project it would be:
    rootProject.subprojects {
        args 'java:classpath::'+it.name+'/build/classes/main'
        args 'java:classpath::'+it.name+'/build/classes/test'
    }
    */

    args '.git'
}

task(jqaanalyze, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'analyze'
}

task(jqaavailablescopes, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'available-scopes'
}

task(jqareset, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'reset'
}

task(jqaeffectiverules, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'effective-rules'
}

task(jqaavailablerules, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'available-rules'
}

task(jqareport, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'report'
}

task(jqa, dependsOn: ['jqascan','jqaanalyze']) {
    jqaanalyze.mustRunAfter jqascan
}

task(jqs, type: JavaExec) {
    classpath = configurations.jqa

    main = 'com.buschmais.jqassistant.commandline.Main'
    args 'server'

    standardInput = System.in
}

clean.dependsOn jqaclean

If you put it into a (reusable) jqa.gradle file, you only need one additional line in your build.gradle:

1
apply from: 'jqa.gradle'

You find a working Gradle Blueprint on GitHub

There is the common saying “Only the code tells the truth.” Yes - but the code tells not the whole truth. It is not only my personal opinion. I hear that often said by Grady Booch and Simon Brown. Therefore a little bit of architecture documentation pays off sooner or later. And if the information from architecture documentation is used to enrich the graph of scanned code, more and better constraints can be checked. So it is very likely that the ROI is sooner, not later.

What is the idea?

As my frequent blog readers know, I like the toolchain of jQAssistant, Neo4j, Asciidoc, PlantUML, and arc42. So when I already scanned the project into a Neo4j database and if there is more truth in the arc42 architecture documentation, it can’t be that hard to enrich the graph with information from the documentation, no?

The idea of “Building Higher-Level Abstractions of Source Code” or using Java Packages as layers/modules/components for analyzing dependencies between business subdomains is one key point of jQAssistant. My pain point with the “traditional” approach is, that the technical thing of using Cypher to manipulate the graph and the “business” thing (ok, here the business is architecture validation which is also quite technical, hence the quotation marks) are mixed. Let me quote Markus Harrer here to illustrate that. In his awesome post about “Building Higher-Level Abstractions of Source Code” he adds the concept of “Subdomains” to the graph using this Cypher statement:

    UNWIND [
        { name: "Clinic" },
        { name: "Owner" },
        { name: "Person" }, 
        { name: "Pet" },
        { name: "Specialty" },
        { name: "Vet" }, 
        { name: "Visit" }
    ]
    AS properties
    CREATE (s:Subdomain) SET s = properties
    WITH s
        MATCH (t:Type)
            WHERE t.name CONTAINS s.name
        MERGE (t)-[:BELONGS_TO]->(s)
    RETURN s.name, t.name

Not very complicated and Cypher is nice to read, but I’m sure we can do better. To borrow some words from the Domain Specific Languages guys: We can stay in the language of describing architectures and make the language of Cypher and the Cypher query itself a reusable implementation detail.

Example: Mark Used Design Patterns via Regular Expressions

I’ve chosen an example that is very similar to Markus’, but a little bit more fine grained: let’s assume we are using a set of patterns in our design and some classes that implement a certain pattern can be found via regular expressions. In the simplest case a Command pattern has the suffix “Command”, a Factory pattern has the suffix “Factory” and so on.

Let’s also assume that the Pattern Language is defined and documented in Architecture Documentation this way:

Screenshot of Pattern Table

Now how can we set a Label of “Plugin” on all plugin classes, label command classes with “Command” and so on? As you will see, it needs only three easy steps.

First step: scan the documentation

As noted in the beginning, I like arc42 and Asciidoc, so the Plugin for reading Asciidoc documents. does the job here. Note that this plugin uses the same version of AsciidoctorJ as jQAssistant does to prevent runtime conflicts. I’m using here the current versions 1.3.0 of jQAssistant and 1.3.3 of the Asciidoc plugin.

To use the Asciidoc plugin, add it as jQAssistant dependency:

Maven

1
2
3
4
5
    <dependency>
        <groupId>de.kontext-e.jqassistant.plugin</groupId>
        <artifactId>jqassistant.plugin.asciidoc</artifactId>
        <version>1.3.3</version>
    </dependency>

Gradle

1
	jqaRt("de.kontext-e.jqassistant.plugin:jqassistant.plugin.asciidoc:1.3.3")

Once we brought the code and the documentation next to each other in the same database, we are on a good way and ready for the next step:

Second step: Finding the relevant information in the documentation

Since version 1.3.3 the Asciidoc plugin also imports attributes on tables, sections, and lists. We can mark our table shown in the picture in the documentation with an attribute label=”Pattern” Then the Asciidoc source looks like this:

    .Design Pattern of Classes Matched by Regular Expressions
    [options="header", label="Pattern"]
    |===
    | Regex                                 | Pattern
    | de.kontext_e.jqassistant.*Plugin      | Plugin
    | de.kontext_e.jqassistant.*Command     | Command
    | de.kontext_e.jqassistant.*Factory     | Factory
    |===

To make the live easier for later Cypher queries, let’s add some labels to the table cells we are interested in. The jQAssistant concept in Asciidoc form looks like this:

    [[structure:MarkAsciidocTypeRegex]]
    [source,cypher,role=concept]
    .Mark Asciidoc Table Cells that contain regular expressions for types that have to be marked with given labels.
    ----
        MATCH
            (a:Asciidoc:Table)-[:BODY]->(body),
            (a)-[:HAS_ATTRIBUTE]->(att:Asciidoc:Attribute)
        WHERE
            att.name='label' AND att.value='Pattern'
        WITH
            body
        MATCH
            (body)-[:CONTAINS_CELLS]->(regexCell:Asciidoc:Cell {colnumber: 0}),
            (body)-[:CONTAINS_CELLS]->(labelCell:Asciidoc:Cell {colnumber: 1})
        SET
            regexCell:RegularExpressionCell,
            labelCell:LabelCell
        CREATE UNIQUE
            (regexCell)-[:REGEX_FOR_LABEL]->(labelCell)
        RETURN
            regexCell, labelCell
    ----

Don’t get confused here. There are two Asciidoc directories in this made up example project:

  • one contains the arc42 architecture documentation of the project
  • the other contains the jQAssistant concepts and constraints which could also be written in XML

So the table is located in arc42, the concept is located in jQAssistant document. You see that now the technical thing and the “business” thing is not mixed anymore.

Ok, we are only one step away from our goal and need only to:

Third step: Set the Pattern Names from Table as Labels

Well, there is a little obstacle here: the names of the labels to be set are stored as properties in the nodes we labeled with “LabelCell” in the query above. And we can’t set labels with property values. But a little known feature of jQAssistant comes to the rescue: jQAssistant allows the use of scripting languages in concepts. Wow! Did you know that? Awesome, the graph cannot only manipulated with Cypher, also JavaScript, Ruby, Groovy, whatever scripting language is available in the classpath can be used. JavaScript works with every newer JDK out of the box, so let’s use this and craft a jQAssistant concept:

    [[structure:LabelTypesMatchedByRegex]]
    [source,js,role=concept,requiresConcepts="structure:MarkAsciidocTypeRegex"]
    .Mark types with labels matched by regex.
    ----
        var graphDatabaseService = store.getGraphDatabaseService();
        // Define the columns returned by the constraint
        var columnNames = java.util.Arrays.asList("Type");
        // Define the list of rows returned by the constraint
        var rows = new java.util.ArrayList();
    
        var result = graphDatabaseService.execute("    MATCH\n" +
                                                           "        (type:Type),\n" +
                                                           "        (regexCell:RegularExpressionCell)-[:REGEX_FOR_LABEL]->(labelCell:LabelCell)\n" +
                                                           "    WHERE\n" +
                                                           "        type.fqn =~ regexCell.text\n" +
                                                           "    RETURN\n" +
                                                           "        type, labelCell.text as label\n");
    
        while(result.hasNext()) {
            var next = result.next();
            var node = next.get("type");
            var label = next.get("label");
            node.addLabel(org.neo4j.graphdb.DynamicLabel.label(label));
            var resultRow = new java.util.HashMap();
            resultRow.put("Class", node);
            rows.add(resultRow);
        }
    
        // Return the result
        var status = com.buschmais.jqassistant.core.analysis.api.Result.Status.SUCCESS;
        new com.buschmais.jqassistant.core.analysis.api.Result(rule, status, severity, columnNames, rows);
    ----

It boils down to two steps:

  • find the Java Type nodes and Asciidoc table cells using a Cypher query
  • iterate over the result and use node.addLabel to set the label at the Java Type nodes

Note that again the jQAssistant stuff does not contain any project specific information. As long as some conventions are matched like marking the table with label=”Pattern” the jQAssistant magic can be reused in every project and all the relevant and project specific information is contained in the architecture documentation.

In the last post I described in detail how to use the same document to check the defined against the actual architecture. If you tried it out with the example project, you found some dependencies in the wrong direction. And perhaps also in your own projects. This time I’ll show you how to handle this Technical Debt in a way that both the CI build gets green again and it is also documented in the arc42 architecture documentation.

Documenting the unwanted dependencies

To make it visible and clear that there is Technical Debt regarding building block dependencies, it is a good idea to document the unwanted dependencies just below the description of the building blocks and the desired dependencies. I used for that an Asciidoc table like this:

.Unwantend Module Dependencies
[options="header"]
|===
| From                          | To                          | What should be done
| de.kontext_e.demo.business    | de.kontext_e.demo.core      | Because ...; Todo: ...
| de.kontext_e.demo.exporter    | de.kontext_e.demo.facade    | Because ...; Todo: ...
| de.kontext_e.demo.exporter    | de.kontext_e.demo.processor | Because ...; Todo: ...
| de.kontext_e.demo.importer    | de.kontext_e.demo.parser    | Because ...; Todo: ...
|===

and it gets rendered this way:

rendered Technical Debt table

In a real project I found it useful the have a third column to describe why the wrong dependency is currently there and to propose refactorings.

Add Asciidoc documents to the jQAssistant database

The freshly released version 1.2.0 of the Kontext E jQAssistant plugin suite contains also a Plugin for reading Asciidoc documents. Note that this plugin uses the same version of AsciidoctorJ as jQAssistant does to prevent runtime conflicts. So this Plugin works only with 1.2+ versions of jQAssistant because former versions use a too old AsciidoctorJ version.

To use the Asciidoc plugin, add it as jQAssistant dependency:

Maven

1
2
3
4
5
    <dependency>
        <groupId>de.kontext-e.jqassistant.plugin</groupId>
        <artifactId>jqassistant.plugin.asciidoc</artifactId>
        <version>${jqassistant.kePlugins.version}</version>
    </dependency>

Gradle

1
	jqaRt("de.kontext-e.jqassistant.plugin:jqassistant.plugin.asciidoc:1.2.0")

The documentation folder is already part of the pathes to be scanned. If it works, you should now have lots of :Asciidoc labeled nodes in your database.

Add a jQAssistant Concept for the Technical Debt

Next thing is to mark dependencies between Java packages as Technical Debt. So I first find our Asciidoc table which I titled “Unwantend Module Dependencies” and use the body:

MATCH 
    (a:Asciidoc:Table)-[:BODY]->(body)
WHERE 
    a.title='Unwantend Module Dependencies'
WITH 
    body

In the first two columns I put the source and target package names, so select them:

MATCH 
    (c1:Asciidoc:Cell {colnumber: 0})<-[:CONTAINS_CELLS]-(body)-[:CONTAINS_CELLS]->(c2:Asciidoc:Cell {colnumber: 1})
WITH 
    c1, c2

Now I find the matching Java packages:

MATCH
    (m1:Java:Package), (m2:Java:Package)
WHERE
    m1.fqn = c1.text
AND
    m2.fqn = c2.text

and create a relationship between them:

MERGE
    (m1)-[:TECHNICAL_DEBT]->(m2)
RETURN
    m1, m2;

The resulting Cypher query is this:

MATCH 
    (a:Asciidoc:Table)-[:BODY]->(body)
WHERE 
    a.title='Unwantend Module Dependencies'
WITH 
    body
MATCH 
    (c1:Asciidoc:Cell {colnumber: 0})<-[:CONTAINS_CELLS]-(body)-[:CONTAINS_CELLS]->(c2:Asciidoc:Cell {colnumber: 1})
WITH 
    c1, c2
MATCH
    (m1:Java:Package), (m2:Java:Package)
WHERE
    m1.fqn = c1.text
AND
    m2.fqn = c2.text
MERGE
    (m1)-[:TECHNICAL_DEBT]->(m2)
RETURN
    m1, m2;

Now I can put the query into the jQAssistant rules file. As usual I show examples for Asciidoc as well as for XML.

Asciidoc

[[documented:TechnicalDebt]]
.Creates a relationship between two Packages for Technical Debt.
[source,cypher,role=concept]
----
MATCH 
    (a:Asciidoc:Table)-[:BODY]->(body)
WHERE 
    a.title='Unwantend Module Dependencies'
WITH 
    body
MATCH 
    (c1:Asciidoc:Cell {colnumber: 0})<-[:CONTAINS_CELLS]-(body)-[:CONTAINS_CELLS]->(c2:Asciidoc:Cell {colnumber: 1})
WITH 
    c1, c2
MATCH
    (m1:Java:Package), (m2:Java:Package)
WHERE
    m1.fqn = c1.text
AND
    m2.fqn = c2.text
MERGE
    (m1)-[:TECHNICAL_DEBT]->(m2)
RETURN
    m1, m2;
----

XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    <concept id="documented:TechnicalDebt">
        <description>
            Creates a relationship between two Packages for Technical Debt.
        </description>
        <cypher><![CDATA[
            MATCH 
                (a:Asciidoc:Table)-[:BODY]->(body)
            WHERE 
                a.title='Unwantend Module Dependencies'
            WITH 
                body
            MATCH 
                (c1:Asciidoc:Cell {colnumber: 0})
                <-[:CONTAINS_CELLS]-(body)-[:CONTAINS_CELLS]->
                (c2:Asciidoc:Cell {colnumber: 1})
            WITH 
                c1, c2
            MATCH
                (m1:Java:Package), (m2:Java:Package)
            WHERE
                m1.fqn = c1.text
            AND
                m2.fqn = c2.text
            MERGE
                (m1)-[:TECHNICAL_DEBT]->(m2)
            RETURN
                m1, m2;
        ]]></cypher>
    </concept>

Modify the jQAssistant Constraint WrongDirection

I modified the Constraint WrongDirection in a way that it also requires “documented:TechnicalDebt” and that the Cypher query excludes documented Technical Debt dependencies:

Asciidoc

[[dependency:WrongDirection]]
.Finds package dependencies which are in the wrong direction according to the documentation.
[source,cypher,role=constraint,requiresConcepts="dependency:TransitivePackageDependencies, documented:TechnicalDebt",severity=critical]
----
MATCH
    (p1:PlantUml:Package)-[:MAY_DEPEND_ON]->(p2:PlantUml:Package),
    (p3:Java:Package)-[:DEPENDS_ON]->(p4:Java:Package)
WHERE
    p1.fqn = p4.fqn
    AND p2.fqn = p3.fqn
    AND NOT (p3)-[:TECHNICAL_DEBT]->(p4)
RETURN
    p3.fqn + "-->" + p4.fqn AS WrongDirection;
----

XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    <constraint id="dependency:WrongDirection" severity="critical">
        <requiresConcept refId="dependency:Package"/>
        <requiresConcept refId="dependency:TransitivePackageDependencies"/>
        <requiresConcept refId="dependency:WrongDirection"/>
        <description>
            Finds package dependencies which are in the wrong direction 
            according to the documentation.
        </description>
        <cypher><![CDATA[
            MATCH
                (p1:PlantUml:Package)-[:MAY_DEPEND_ON]->(p2:PlantUml:Package),
                (p3:Java:Package)-[:DEPENDS_ON]->(p4:Java:Package)
            WHERE
                p1.fqn = p4.fqn
                AND p2.fqn = p3.fqn
                AND NOT (p3)-[:TECHNICAL_DEBT]->(p4)
            RETURN
                p3.fqn + "-->" + p4.fqn AS WrongDirection;
        ]]></cypher>
    </constraint>

Running the checks again

If you run the checks in the example project again, the four constraint violations should be gone.

Some closing words

If dependencies in a project got never checked, there are most like unwanted dependencies. Not surprising and it also hit me with the C++ project I wrote about Breaking the build with any wrong dependency makes the build red for a long time. Resolving those dependencies is not a five minute task. But documenting them and defining exceptions from the dependency architecture rules with exactly the same table gives us a good chance to improve the architecture step by step over several iterations.

As you may have guessed from the title, more posts about handling Technical Debt are in the pipeline. But before that, I have to introduce you to some other things first.

Some while ago I wrote how we make sure that the documented and the actual architecture match using the idea of Executable Architecture Documentation. This time I want to re-visit this again, but now a little bit more as a tutorial.

An ultra-short recapitulation of the idea

arc42 is a widely used template for architecture documentation. Chapter 5 describes the Building Block View UML Package diagrams can be used to picture the building blocks. There is the current version of the arc42 template on GitHub. Out of the template in Asciidoc several other formats can be generated, but we go with Asciidoc. PlantUML is a Domain Specific Language to describe UML diagrams in textual notation and tooling to make standalone or embedded graphics out of this. jQAssistant scans Java projects into a Neo4j database and can be easily extended by plugins. Kontext E provides a Plugin for reading PlantUML package diagrams.

So the main building blocks described in the architecture documentation can be put into the same database where the actual architecture artifacts are located. Only one simple Cypher query is needed to compare the documented and actual package dependencies.

Add the PlantUML plugin to the jQAssistant configuration

First we need to add the Kontext E PlantUML plugin to jQAssistant in the build file. In the following sections I’ll give examples for Maven and Gradle

Maven

As described in the jQAssistant documentation you add another dependency to the jQA Maven plugin:

1
2
3
4
5
6
7
    <dependencies>
        <dependency>
            <groupId>de.kontext-e.jqassistant.plugin</groupId>
            <artifactId>jqassistant.plugin.plantuml</artifactId>
            <version>1.1.4</version>
        </dependency>
    </dependencies>

Gradle

Given you have a separate configuration for jQAssistant plugins:

1
2
3
    configurations {
        jqaRt
    }

you add the PlantUML plugin this way:

1
	jqaRt("de.kontext-e.jqassistant.plugin:jqassistant.plugin.plantuml:1.1.4")

Put the PlantUML diagram into arc42 Building Block View chapter

It is quite easy to put a UML diagram into an Asciidoc file. For the jQA PlantUML plugin itself it could look like this:

    ["plantuml","MainBuildingBlocks.png","png"]
    -----
    package de.kontext_e.jqassistant.plugin.plantuml.scanner {}
    package de.kontext_e.jqassistant.plugin.plantuml.store {
        package de.kontext_e.jqassistant.plugin.plantuml.store.descriptor{}
    }
    
    de.kontext_e.jqassistant.plugin.plantuml.scanner ---> de.kontext_e.jqassistant.plugin.plantuml.store
    
    -----

It gets rendered into a UML Package Diagram:

UML Package Diagram.

As you noticed, the example is from the jQAssistant PlantUML plugin itself. The rendered Asciidoc example Building Block View chapter looks like this:

Rendered Asciidoc Building Block View

Add the architecture documentation directory to the scan path

To make jQA also scan the architecture documentation, it must be configured to look into the very same directory.

Maven

Given the architecture documentation resides in ‘doc/architecture’, simply add this folder as a scan target like this:

1
2
3
4
5
6
7
    <configuration>
        <scanIncludes>
            <scanInclude>
                <path>doc/architecture</path>
            </scanInclude>
        </scanIncludes>
    </configuration>

Gradle

This is a snippet for scanning a multi-module Gradle project, again with an additional directory to scan for the architecture documentation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    task(jqascan, type: JavaExec) {
      main = 'com.buschmais.jqassistant.scm.cli.Main'
      classpath = configurations.jqaRt
      args 'scan'
      args '-p'
      args 'jqassistant/jqassistant.properties'
      args '-f'
    
      rootProject.subprojects {
        args 'java:classpath::'+it.name+'/build/classes/main'
        args 'java:classpath::'+it.name+'/build/classes/test'
        args it.name+'/build/reports'
        args it.name+'/src/main'
        args it.name+'/src/test'
      }
    
      args 'doc/architecture'
    }

After scanning the project we get a graph like this:

Graph in Neo4j browser

That’s a screenshot of the Neo4j graph browser which I pimped a little bit to make it more expressive. If you try it out exactly at this point, you will notice that in the original scan there is no dependency from scanner to store in the real architecture. This brings us directly to the following section.

jQAssistant Concepts and Constraints

Now as all information is put into one database, expected and actual state can be matched using jQA Concepts and Constraints. In jQAssistant, Concepts and Constraints can be given in XML and Asciidoc. I’ll give the examples for both of them.

XML

In the style of this XML example we add the following snippets.

First, we need a package level as described here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    <concept id="package:PackageLevel">
        <requiresConcept refId="dependency:Package"/>
        <description>
            Set the level property of a package, 
            e.g. 1 for de, 2 for de.kontext_e and so on
        </description>
        <cypher><![CDATA[
			MATCH
				(p:Java:Package)
			WITH
				SPLIT(p.fqn,".") AS splitted, p
			SET
				p.level=SIZE(splitted)
			RETURN
				splitted, SIZE(splitted);
		]]></cypher>
    </concept>

This package levels are used to add some transitive package dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    <concept id="dependency:TransitivePackageDependencies">
        <requiresConcept refId="package:PackageLevel"/>
        <requiresConcept refId="dependency:Package"/>
        <description>
            Add a DEPENDS_ON relationship to parents of a package P 
            from other packages up to the same level of the source package.
        </description>
        <cypher><![CDATA[
            MATCH
                (p:Java:Package)-[:DEPENDS_ON]->(p2:Java:Package),
                (parent:Java:Package)-[:CONTAINS*]->(p2:Java:Package)
            WHERE
                p.level <= parent.level
            CREATE UNIQUE
                (p)-[:DEPENDS_ON]->(parent)
            RETURN
                p.fqn, parent.fqn;
        ]]></cypher>
    </concept>

which come handy to find package dependencies in the wrong direction with this little Constraint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    <constraint id="dependency:WrongDirection" severity="critical">
        <requiresConcept refId="dependency:Package"/>
        <requiresConcept refId="dependency:TransitivePackageDependencies"/>
        <description>
            Finds package dependencies which are in the wrong direction 
            according to the documentation.
        </description>
        <cypher><![CDATA[
            MATCH
                (p1:PlantUml:Package)-[:MAY_DEPEND_ON]->(p2:PlantUml:Package),
                (p3:Java:Package)-[:DEPENDS_ON]->(p4:Java:Package)
            WHERE
                p1.fqn = p4.fqn
                AND p2.fqn = p3.fqn
            RETURN
                p3.fqn + "-->" + p4.fqn AS WrongDirection;
        ]]></cypher>
    </constraint>

Don’t forget to add all of that to the default group:

1
2
3
4
5
6
7
    <group id="default">
        <includeConcept refId="package:PackageLevel"/>
        <includeConcept refId="dependency:TransitivePackageDependencies"/>
        <includeConstraint refId="dependency:WrongDirection" 
                           severity="critical"/>
    </group>

That’s it.

Asciidoc

And now in the style of the Asciidoc example:

    [[default]]
    [role=group,includesConstraints="dependency:WrongDirection(critical)"]
    - <<package:PackageLevel>>
    - <<dependency:TransitivePackageDependencies>>
    - <<dependency:WrongDirection>>
    [[package:PackageLevel]]
    .Set the level property of a package, e.g. 1 for de, 2 for de.kontext_e and so on.
    [source,cypher,role=concept,requiresConcepts="dependency:Package"]
    ----
    MATCH
        (p:Java:Package)
    WITH
        SPLIT(p.fqn,".") AS splitted, p
    SET
        p.level=SIZE(splitted)
    RETURN
        splitted, SIZE(splitted);
    ----
    [[dependency:TransitivePackageDependencies]]
    .Add a DEPENDS_ON relationship to parents of a package P from other packages up to the same level of the source package.
    [source,cypher,role=concept,requiresConcepts="package:PackageLevel"]
    ----
    MATCH
        (p:Java:Package)-[:DEPENDS_ON]->(p2:Java:Package),
        (parent:Java:Package)-[:CONTAINS*]->(p2:Java:Package)
    WHERE
        p.level <= parent.level
    CREATE UNIQUE
        (p)-[:DEPENDS_ON]->(parent)
    RETURN
        p.fqn, parent.fqn;
    ----
    [[dependency:WrongDirection]]
    .Finds package dependencies which are in the wrong direction according to the documentation.
    [source,cypher,role=constraint,requiresConcepts="dependency:TransitivePackageDependencies",severity=critical]
    ----
    MATCH
        (p1:PlantUml:Package)-[:MAY_DEPEND_ON]->(p2:PlantUml:Package),
        (p3:Java:Package)-[:DEPENDS_ON]->(p4:Java:Package)
    WHERE
        p1.fqn = p4.fqn
        AND p2.fqn = p3.fqn
    RETURN
        p3.fqn + "-->" + p4.fqn AS WrongDirection;
    ----

Running the check

No special things have to be done to run the actual check. It is done in the normal jQAssistant run. So it works on the local machine as well as in the CI process quite easily.

Making HTML (or PDF) out of Asciidoc

One widely used toolchain is Asciidoctor with Asciidoctor Diagram to generate a nice set of HTML documents. There is also Asciidoctor PDF for a direct generation of PDF.

A complete example

These were a lot of snippets. Therefore I created a very basic example that contains all to make it run but nothing else. You can download the sources on GitHub, explore and play around.

If you do a

1
mvn verify

you’ll find four constraint violations.

Stay tune for follow up posts. They explain why the example is called “Uneven Modules” and what can be done for taming the architecture with jQAssistant.

Some closing words

There are of course many other arc42 chapters. Some of them are not suited to be checked automatically, but others may contain also checkable contents like the Design Decisions chapter. For example rules like ‘no java.util.Date anymore’ or ‘use log4j instead of java.util.logging’ are quite easy to enforce.

The Concepts and Constraints above do not cover two things:

  • packages that were found but not documented
  • packages that were documented but not found

I’m not sure if that is needed and/or useful. Everyone should decide this for the concrete project situation.

So now the CI build creates HTML/PDF architecture documentation and uses the same document to check the defined against the actual architecture. How crazy is this?