Skip to content

Yet another way to decrease pain during Java project documentation

Igor Maznitsa edited this page Jun 3, 2023 · 3 revisions

During my career, very often I saw projects that were absolutely non-transparent, and it was very hard to detect "input", "output" and "processing parts" in them. It was very common to hear, "Some programmer developed that, but then he left the company, and we don't have contacts, and nobody knows how it works". Also, it was often a situation in my own projects when switching between many of them, and in some moments you can just forget what you did in a project and be surprised to see yourself as the author of changes, but you can't recall them. In this case, we need some maps of the project, just like sailors and pirates wanted maps of the seas and treasures.

Preword

In 2015, I was working in a company that didn't spend a lot of time on project documentation, and I made my own IDE plugin and editor to keep project documentation and records just among project sources. My solution uses a mind-mapping approach and saves documentation in Markdown compatible text format, called MMD in this case.

My mind-map processing plugin allows me to create and edit them, but there can be some problems with file links in mind-maps that can lose their targets during refactoring. I had the idea to add support for refactoring the API of IDEs, but it looks like too much work and support. So, I decided to use a standard Java approach: annotation processors. It provides an IDE-independent way to work with sources in Java.
image
So, I made a set of annotations to mark important parts of sources and generated mind-maps to show the marked places on the generated maps with a special annotation processor. An annotation processor can be easily embedded into the build process for Maven and Gradle, for instance.

Well, let's see how my solution works with a well-known Sprint test project called Pet Clinic. I will show how to mark sources and inject annotation processors into the Maven build process.

Prepare project

For the first one, let's clone the example project from GitHub.

git clone [email protected]:spring-projects/spring-petclinic.git

The project allows to use both Gradle and Maven, but in the tutorial I am going to show only the Maven way (but you can find info for Gradle in Wiki).

Let's build the project to be sure that it works. In Maven, you can use it with mvn package (it should be executed in the folder that contains pom.xml).

mvn package

Add dependencies

As the first one, we should add dependencies into pom.xml (it is a Maven project file descriptor).

<dependency>
  <groupId>com.igormaznitsa</groupId>
  <artifactId>mind-map-annotations</artifactId>
  <version>1.6.3</version>
  <scope>provided</scope>
</dependency>

The added dependency contains all required annotation sets, all of which are marked as source level only, which means that the annotations will not be presented in compiled files.

Compiler tuning

The next step is to create a special Maven profile that will execute the annotation processor during the compilation phase. The processor extracts information from our annotations and creates documents if the profile is activated. The profile is needed to avoid having the annotation processor call every build.

<profile>
  <id>mmddoc</id>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <annotationProcessorPaths combine.children="append">
            <annotationProcessorPath>
              <groupId>com.igormaznitsa</groupId>
              <artifactId>mind-map-annotation-processor</artifactId>
              <version>1.6.3</version>
            </annotationProcessorPath>
          </annotationProcessorPaths>
        </configuration>
      </plugin>
    </plugins>
  </build>
</profile>

The code snippet above adds Maven-profile mmddoc which can be activated through the profile list during build.

mvn clean package -Pmmddoc

So let's execute the command. Our project was built well, but we don't see any changes.

Let the Mapping Begin

The first important annotation that should be added is com.igormaznitsa.mindmap.annotations.MmdFile. The annotation starts the magic and tells the annotation processor to create a MMD document. Let's add it to the main starting application class, __ PetClinicApplication__. But we should also add some attributes to the annotation.

@MmdFile(fileName = "PetClinic", uid = "MAIN_APP", 
         rootTopic = @MmdTopic(title = "Pet clinic application"))
@SpringBootApplication
@ImportRuntimeHints(PetClinicRuntimeHints.class)
public class PetClinicApplication {

	public static void main(String[] args) {
		SpringApplication.run(PetClinicApplication.class, args);
	}

}

We have provided the target file name PetClinic, added some unique identifier MAIN_APP for the target document, and defined the root mind-map element with the text Pet clinic application. Let's try building the project with the mmddoc profile again, everything looks working, and we get a new MMD file just next to the source file where we defined the annotation. The file can be rendered by special plugins for NetBeans, Intellij IDEA, or SciaReto editor.   image

Tune file link paths

We can see some file link icons on the root element of the generated mind map file, but when we click on them, we get an error message that the file was not found. It is because we need to tune relative paths for the annotation processor. The IDE plugin expects that links will be relative to the project root folder. Let's add some special parameters for the annotation processor to provide the project root folder. Because it is a single-module project, we can just use project.basedir.

<compilerArgs combine.children="append">
  <compilerArg>-Ammd.file.link.base.folder=${project.basedir}</compilerArg>
</compilerArgs>

Let's rebuild our project again, and now we have a correctly working link in the MMD file. We can navigate through projects by just clicking on the file link on the mind map root, and the IDE shows the file and place where the annotation is provided.

Add more info on the map

It is time to enrich our map and add more important information to it, for instance, information about HTTP controllers and their published methods. Let's start with the class VetController, which describes vets. Go to the class and add annotations to its header.

@MmdFileRef(uid = "MAIN_APP")
@MmdTopic(title = "Vet", path = { "Controllers" })
@Controller
class VetController {
...
}

What do they mean? The first one is MmdFileRef. It tells the annotation processor to use the file with the identifier MAIN_APP for all mind map nodes found among child elements. Recall that the identifier we set to the annotation in PetClinicApplication will be in use. The next annotation is MmdTopic. It creates some records on the mind map with the text Vet and a file link exactly to the file place where it is defined. But to highlight "controller" and keep it apart, I added a path with a node titled "Controllers" (the annotation processor will create such a mind map node automatically). The result of the new project build is shown below.

image
Our map now contains important information, and anyone can open it very quickly to figure out where the application root is, and where the Vet controller is. But let's add more information about controllers. The Vet class contains HTTP methods, they should be added to the map. To do it, we should add MmdTopic annotations to the methods in VetController.

@MmdTopic(title = "showVetList", note = "GET /vets.html")
@GetMapping("/vets.html")
public String showVetList(@RequestParam(defaultValue = "1") int page, 
Model model) {
...
}

@MmdTopic(title = "showResourcesVetList", note = "GET /vets")
@GetMapping({ "/vets" })
public @ResponseBody Vets showResourcesVetList() {
...
}

After rebuilding, the document changed. image
So, now, at any moment, we can open our map and find the required HTTP methods with one click.

Add one more controller

Let's add one more controller to the map. It is the controller processing pet owners defined in the OwnerController class. As the first one, we will add an MMD file link to define the target MMD file.

@MmdFileRef(uid = "MAIN_APP")
@MmdTopic(title = "Owner", path = { "Controllers" })
@Controller
class OwnerController {
...
}

We see new information on the map. image
So now our map is much rich one.
image

Lets examine marks for methods

Let examine our mark for method showOwner in class OwnerController

@MmdTopic(title = "showOwner", note = "GET /owners/{ownerId}")
@GetMapping("/owners/{ownerId}")
public ModelAndView showOwner(@MmdTopic(title = "ownerId") 
@PathVariable("ownerId") int ownerId) {
...
}

I would like to highlight that there is a MmdTopic annotation marking the ownerId method argument, and the annotation was added to the map as a child node for the method.

Tune folder for generated documents

Currently, MMD documents are generated just in the source folder among Java files. It is not good to mix generated files and sources, so we should move them into a special folder. Let's add two additional parameters to the annotation configuration.

<compilerArg>-Ammd.target.folder=${project.basedir}/.projectKnowledge</compilerArg>
<compilerArg>-Ammd.folder.create=true</compilerArg>

The first parameter defines the folder where we're going to keep the generated files. I have chosen .projectKnowledge because historically, my ID plugins have automatically recognized the folder. image So above, I have described the minimal use of the tool. It is possible to change map node colors, add URLs, emoticons, and internal jumps between nodes. It is very important to understand that now that we have some project maps, they can be refreshed at any moment or even every build, and anyone can quickly find information about implementation places. Any changes in source file names or refactoring after a mind map rebuild will be correctly represented.

Multi-module project processing

PetClinic is an easy single module project. It is a very rare occurrence in the Java world. Usually, developers work with multi-module, deeply hierarchical projects. My IDE plugins calculate paths relative to the project root, and as the root, they use the top project folder. It is not very compatible with the use of the maven parameter project.basedir and needs a more complex approach. To get the top project folder, I use a special Maven plugin. If we improve our Maven profile to process multi-module hierarchies, it will look like this:

<profile>
  <id>mmddoc</id>
  <properties>
    <mmdDoc.folder>.projectKnowledge</mmdDoc.folder>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>org.commonjava.maven.plugins</groupId>
        <artifactId>directory-maven-plugin</artifactId>
        <version>1.0</version>
        <executions>
          <execution>
            <id>directories</id>
            <goals>
              <goal>highest-basedir</goal>
            </goals>
            <phase>initialize</phase>
            <configuration>
              <property>mmd.basedir</property>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <annotationProcessorPaths combine.children="append">
            <annotationProcessorPath>
              <groupId>com.igormaznitsa</groupId>
              <artifactId>mind-map-annotation-processor</artifactId>
              <version>1.6.3</version>
            </annotationProcessorPath>
          </annotationProcessorPaths>
          <compilerArgs combine.children="append">
            <compilerArg>-Ammd.file.link.base.folder=${mmd.basedir}</compilerArg>
            <compilerArg>-Ammd.target.folder=${project.basedir}${file.separator}${mmdDoc.folder}</compilerArg>
            <compilerArg>-Ammd.folder.create=true</compilerArg>
          </compilerArgs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</profile>

The directory-maven-plugin finds the highest project folder in hierarchy and places the found folder into the mmd.basedir property. Also, it is useful to add folder cleaning.

<plugin>
  <artifactId>maven-clean-plugin</artifactId>
  <configuration>
    <filesets>
      <fileset>
        <directory>${project.basedir}${file.separator}${mmdDoc.folder}</directory>
        <followSymlinks>false</followSymlinks>
      </fileset>
    </filesets>
  </configuration>
  <executions>
    <execution>
      <id>clear-mmd-folder</id>
      <phase>initialize</phase>
      <goals>
        <goal>clean</goal>
      </goals>
      <configuration>
        <excludeDefaultDirectories>true</excludeDefaultDirectories>
      </configuration>
    </execution>
  </executions>
</plugin>