Ein Doclet zur Erzeugung von DocBook aus Javadoc

14.02.2025

Ich habe mich mit der Idee zu diesem Projekt Monate abgequält - hätte ich gewusst, was die eigentliche Implementierung für Qualen verursachen würde, hätte ich sie wahrscheinlich eingestampft.

Dieses Projekt war in meiner Erinnerung das schmerzhafteste, das ich bisher angegangen bin - aber von Anfang an...

Die Idee

Ich habe für einige meiner Projekte Dokumentationen verfasst, die über das Schreiben einer Readme hinausgehen - ob es sich um die sQLshell handelt, um The Ultimate RISC oder um dWb+ - um nur einige zu nennen. Für die sQLshell habe ich mich dazu noch für JavaHelp entschieden, aber ansonsten nutze ich eigentlich jetzt immer DocBook.

Für (Java) Projekte, die eine Scripting-API anbieten ist es schön, automatisch Dokumentation aus den Javadoc-Kommentaren erzeugen zu können - für die sQLshell konnte ich das sofort benutzen, da das Default-Ergebnis des Javadoc-Processing HTML ist und das ist dieselbe Technologie, die auch hinter JavaHelp steht. Für meine Projekte deren Dokumentation auf DocBook beruht wäre es schön, wenn man eine entsprechende Technologie hätte, die die Javadoc-Kommentare nach DocBook umsetzen - damit könnte man dann die API in allen von DocBook unterstützten Zielformaten erzeugen.

Javadoc bietet die Möglichkeit, sogenannte Doclets für andere als das Default-Zielformat zu benutzen und sogar die Möglichkeit, neue Tags zu definieren und diese mit dem Default-Zielformat oder anderen Zielformaten zu benutzen. Mit einer solchen Flexibilität ist doch bestimmt bereits einmal jemand auf die Idee gekommen, ein DocBooc Doclet zu schreiben und es als OpenSource zu veröffentlichen? Sollte man denken...

Die Ernüchterung

Ich fand beinahe sofort ein entsprechendes Projekt, konnte aber den (12 Jahre alten) Build nicht nutzen: Die Doclet-API hat sich (wegen der idiotischen Modularisierungsidee in Java) so sehr geändert, dass alte vor-Java-9 Doclets mit Java Version 9 oder neuer nicht mehr funktionieren.

In den commit-Messages war aber zu erkennen, dass das Projekt vor kurzem auf die neue Doclet-API umgestellt worden war - und es gab sogar eine neue Release. Das ließ mich wieder hoffen. Die Hoffnung wurde zerstört: Das Ausführen des Doclet führte zu einer Menge kryptischer Fehlermeldungen und als ich mir den Quelltext ansah stellte ich fest, dass die Migration auf die neue API nicht vollständig war.

Beim Versuch, das Projekt dann eben selbst zu bauen fiel ich dann total vom Glauben ab - nicht nur, dass der Originalautor die Migration auf die neue API nicht auf einem entsprechenden Branch erledigt hat - er hat einfach die betrofenen Klassen kopiert und in ein beinahe namensgleiches Package parallel zu dem ursprünglichen abgelegt, nur mit einer 8 angehängt. Das benutzte Buildsystem ist Apache Ant - nichts dagegen zu sagen, benutze ich auch immer noch. Aber: das toplevel build.xml includiert ein anderes aus einem darüberliegenden Verzeichnis. Damit kann man das Projekt nicht bauen.

Die Herausforderungen

Parsen

Ich fasste also den schweren Entschluss, ein entsporechendes Doclet selbst zu erstellen - getreu dem Motto aus dem Amiga Magazin: "Fehlt Dir was? Programmiers Dir doch!". Die API ließ mich schier verzweifeln. Irgendwann fragte ich mich, ob andere vor mir bereits Doclets für die neue API geschrieben hatten und ich dort eventuell Inspiration finden könnte. Ich fand solche Doclets zum Beispiel für ASCIIDOC oder Markdown - allerdings funktionieren die genau anders herum: Sie erlauben es, die jeweilige Syntax statt der Defaultsyntax in Javadoc-Kommentaren zu benutzen - für mich nutzlos - ich musste für mich allein kämpfen.

Irgendwann war es aber dann so weit: Ich hatte über die API eine Datenstruktur erstellt, die alle von mir für den ersten Wurf definierten Tags und syntaktischen Konstrukte aus dem Javadoc extrahierte - nun konnte es an die Erstellung des DocBook gehen.

Generieren

Zur Generierung benutzte ich Velocity - zumindest hatte ich das vor: Die große Flexibilität von DocBook kommt mit einem Preis: dieses Format ist sehr verbose: Man muss recht viel Text schreiben, um zu brauchbaren Resultaten zu kommen - aber das gilt ja eigentlich für jede von SGML abstammende Markup-Sprache... Das Problem dabei: ich konnte das nicht tun, weil jemand beim Re-Design der API übersehen hatte, dass ja die völlig idiotischen Modularisierungsidee Einzug gehalten hatte - und damit hatte ich dann sobald ich mein neues Doclet benutzen wollte und im Template auf irgendwelche Member der Doclet-Klassen zugreifen wollte mit Fehlermeldungen wie beispielsweise dieser hier zu kämpfen (Grund: Velocity arbeitet viel mit Introspection, auch in der aktuellen Version noch):

Caused by: java.lang.IllegalAccessException: class org.apache.velocity.util.DuckType cannot access \
class com.sun.tools.javac.util.List (in module jdk.compiler) because module jdk.compiler does not export \
com.sun.tools.javac.util to unnamed module @12dae582

Nun musste ich zunächst herausfinden, wie man JVM-Argumente korrekt an javadoc übergibt - Spoiler: -J. Das führte dazu, dass ich meine Kommandozeile/ANT-Datei mit schwarz-magischen Kommandozeilenparametern überfrachten musste - nur um das Doclet überhaupt benutzen zu können:

--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED

Das Template (aktueller Stand) für die derzeit unterstützten Features sieht wie folgt aus:

<?xml version="1.0" encoding="UTF-8"?>
<refentry>
<refnamediv>
<refname>${stringifier.toString(${data.className})}</refname>
<refpurpose>${stringifier.toString(${data.firstSentence})}</refpurpose>
</refnamediv>
<refsynopsisdiv>
    <classsynopsis language="java">
  <ooclass>
    <!--modifier>class</modifier-->
    <package>${stringifier.toString(${data.namespace})}</package>
    <classname>${stringifier.toString(${data.className})}</classname>
  </ooclass>
  #if(${data.methodDescriptions})
  #foreach ($value in ${data.methodDescriptions.values()})
  <methodsynopsis>
    <type>${stringifier.toString(${value.returnType})}</type>
    <methodname><xref endterm="refsect_${data.namespace}_${data.className}_${stringifier.toString(${value.simpleName})}_title" linkend="refsect_${data.namespace}_${data.className}_${stringifier.toString(${value.simpleName})}"/></methodname>
  #if(${value.params})
  #foreach ($param in ${value.methodParameters})
    <methodparam>
	 <type>${stringifier.toString(${param.asType()})}</type>
	 <parameter>${stringifier.toString(${param.simpleName})}</parameter>
    </methodparam>
  #end
  #else
    <void/>
  #end
  </methodsynopsis>
  #end
  #end
</classsynopsis>
</refsynopsisdiv>
#if(${data.authors})
<refsect1><title>Authors</title>
<itemizedlist>
#foreach ($part in ${data.authors})
<listitem>
${stringifier.toString(${part.name})}
</listitem>
#end
</itemizedlist>
</refsect1>
#end
#if(${data.body})
<refsect1><title>Description</title>
<para>
#foreach ($part in ${data.body})
${stringifier.toString(${part})}
#end
</para>
</refsect1>
#end
#if(${data.methodDescriptions})
<refsect1><title>Methods</title>
#foreach ($part in ${data.methodDescriptions})
<refsect2 xml:id="refsect_${data.namespace}_${data.className}_${stringifier.toString(${part.simpleName})}"><title xml:id="refsect_${data.namespace}_${data.className}_${stringifier.toString(${part.simpleName})}_title">${stringifier.toString(${part.simpleName})}</title>
#if(${part.firstSentence})
<para>
#foreach ($sentencepart in ${part.firstSentence})
${stringifier.toString(${sentencepart})}
</para>
#end
#end
<para>
  <methodsynopsis>
    <type>${stringifier.toString(${part.executableElement.returnType})}</type>
    <methodname>${stringifier.toString(${part.executableElement.simpleName})}</methodname>
  #if(${part.executableElement.parameters})
  #foreach ($param in ${part.executableElement.parameters})
    <methodparam>
	 <type>${stringifier.toString(${param.asType()})}</type>
	 <parameter>${stringifier.toString(${param.simpleName})}</parameter>
    </methodparam>
  #end
  #else
    <void/>
  #end
  </methodsynopsis>
</para>
#if(${part.body})
<para>
#foreach ($bodypart in ${part.body})
${stringifier.toString(${bodypart})}
#end
</para>
#end
#if(${part.params})
<refsect3><title>Parameters</title>
<variablelist>
#foreach ($param in ${part.params})
  <varlistentry>
    <term>${stringifier.toString(${param.name})}</term>
    <listitem>
       <para>
       #foreach ($descriptionpart in ${param.description})
       ${stringifier.toString(${descriptionpart})}
       #end
       </para>
    </listitem>
  </varlistentry>
#end
</variablelist>
</refsect3>
#end
#if(${part.throwables})
<refsect3><title>Exceptions</title>
<variablelist>
#foreach ($throwable in ${part.throwables})
  <varlistentry>
    <term>${stringifier.toString(${throwable.exceptionName})}</term>
    <listitem>
       <para>
       #foreach ($descriptionpart in ${throwable.description})
       ${stringifier.toString(${descriptionpart})}
       #end
       </para>
    </listitem>
  </varlistentry>
#end
</variablelist>
</refsect3>
#end
#if($part.returns)
<refsect3><title>Return Value</title>
#foreach ($return in ${part.returns})
<para>
<returnvalue>
#foreach ($descriptionpart in ${return.description})
${stringifier.toString(${descriptionpart})}
#end
</returnvalue>
</para>
#end
</refsect3>
#end
</refsect2>
#end
</refsect1>
#end
</refentry>

Das Ergebnis

Im Ergebnis wird nun für jede Klasse ein gültiges DocBook-Fragment erzeugt, das an einem Beispiel illustriert so aussieht:

<?xml version="1.0" encoding="UTF-8"?>
<refentry>
<refnamediv>
<refname>WorkspaceExporter</refname>
<refpurpose>Dieses Interface ist die Grundlage für die Möglichkeiten, Workspaces in beliebigen Formaten
 direkt aus dWb+ heraus zu exportieren.</refpurpose>
</refnamediv>
<refsynopsisdiv>
    <classsynopsis language="java">
  <ooclass>
    <!--modifier>class</modifier-->
    <package>de.netsysit.dataflowframework.logic.services.workspace</package>
    <classname>WorkspaceExporter</classname>
  </ooclass>
  <methodsynopsis>
    <type>boolean</type>
    <methodname><xref endterm="refsect_de.netsysit.dataflowframework.logic.services.workspace_WorkspaceExporter_save_title" linkend="refsect_de.netsysit.dataflowframework.logic.services.workspace_WorkspaceExporter_save"/></methodname>
    <methodparam>
	 <type>de.netsysit.dataflowframework.logic.services.workspace.WorkspaceExporter.Support</type>
	 <parameter>support</parameter>
    </methodparam>
    <methodparam>
	 <type>de.netsysit.dataflowframework.ui.WorkspaceDescription[]</type>
	 <parameter>wda</parameter>
    </methodparam>
    <methodparam>
	 <type>java.io.OutputStream</type>
	 <parameter>os</parameter>
    </methodparam>
  </methodsynopsis>
  <methodsynopsis>
    <type>java.lang.String</type>
    <methodname><xref endterm="refsect_de.netsysit.dataflowframework.logic.services.workspace_WorkspaceExporter_getSuffix_title" linkend="refsect_de.netsysit.dataflowframework.logic.services.workspace_WorkspaceExporter_getSuffix"/></methodname>
    <void/>
  </methodsynopsis>
</classsynopsis>
</refsynopsisdiv>
<refsect1><title>Authors</title>
<itemizedlist>
<listitem>
elbosso
</listitem>
</itemizedlist>
</refsect1>
<refsect1><title>Description</title>
<para>
Das Interface beschreibt zwei Methoden: eine davon
 enthält den Code zur eigentlichen Serialisierung und die andere dient hauptsächlich der Verankerung in dWb+.
/para
para

Es können mehrere Implementierungen dieses Interface innerhalb der Anwendung registriert sein. Der Anwender startet den Exportprozeß und gibt an, wie die resultierende Datei heißen soll. dWb+ bestimmt dann anhand der Endung der gewählten Export-Datei, welche Implementierung zur Serialisierung herangezogen wird. </para> </refsect1> <refsect1><title>Methods</title> <refsect2 xml:id="refsect_de.netsysit.dataflowframework.logic.services.workspace_WorkspaceExporter_save"><title xml:id="refsect_de.netsysit.dataflowframework.logic.services.workspace_WorkspaceExporter_save_title">save</title> <para> Methode zum Export der Struktur eines Workspace. </para> <para> <methodsynopsis> <type>boolean</type> <methodname>save</methodname> <methodparam> <type>de.netsysit.dataflowframework.logic.services.workspace.WorkspaceExporter.Support</type> <parameter>support</parameter> </methodparam> <methodparam> <type>de.netsysit.dataflowframework.ui.WorkspaceDescription[]</type> <parameter>wda</parameter> </methodparam> <methodparam> <type>java.io.OutputStream</type> <parameter>os</parameter> </methodparam> </methodsynopsis> </para> <para> Diese Methode muß den Inhalt der Workspaces, wie er übergeben wurde transformieren und die transformierte Darstellung in den Strem schreiben, der von der Anwendung mit der vom Anwender gewählten Datei verbunden wurde. </para> <refsect3><title>Parameters</title> <variablelist> <varlistentry> <term>support</term> <listitem> <para> Referenz auf den Workspace, zu dem die zu serialisierenden Objekte gehören. </para> </listitem> </varlistentry> <varlistentry> <term>wda</term> <listitem> <para> Referenz auf ein Array von WorkspaceDescriptions, die die folgenden Angaben enthalten:

itemizedlist

listitem Angaben zu den Modulen als Instanzen /listitem

listitem Angaben zu den Verbindungen zwischen den Modulen /listitem

/itemizedlist

Der Parameter ist als Array gestaltet. In der aktuellen Implementierung ist dieses Array immer mit genau einem Element bestückt. Implementierungen müssen aber dafür Sorge tragen, auch damit umgehen zu können, daß der Parameter

itemizedlist

listitem eine null-Referenz ist /listitem

listitem ein leeres Array ist /listitem

listitem ein Array mit mehr als einem Element ist /listitem

/itemizedlist </para> </listitem> </varlistentry> <varlistentry> <term>os</term> <listitem> <para> Stream, in den das Resultat geschrieben werden soll. Dieser Stream wird von der Implementierung nicht geschlossen. Um das Freigeben der damit verknüpften Ressourcen kümmert sich dr Aufrufer. </para> </listitem> </varlistentry> </variablelist> </refsect3> <refsect3><title>Return Value</title> <para> <returnvalue> Aussage, ob der Exportvorgang erfolgreich abgeschlossen werden konnte </returnvalue> </para> </refsect3> </refsect2> <refsect2 xml:id="refsect_de.netsysit.dataflowframework.logic.services.workspace_WorkspaceExporter_getSuffix"><title xml:id="refsect_de.netsysit.dataflowframework.logic.services.workspace_WorkspaceExporter_getSuffix_title">getSuffix</title> <para> Diese Methode dient der Integration einer Klasse, die dieses Interface implementiert in die Anwendung dWb+: Es besteht die Möglichkeit, daß mehrere Implementierungen in der Anwendung registriert sind. </para> <para> <methodsynopsis> <type>java.lang.String</type> <methodname>getSuffix</methodname> <void/> </methodsynopsis> </para> <para> dWb+ entscheidet dann anhand der Übereinstimmung der Endung des vom Anwender gewählten Namens der Export-Datei mit dem Rückgabewert dieser Methode, welche der Implementierungen tatsächlich benutzt wird, um die Ergebnisdatei zu erzeugen. </para> <refsect3><title>Return Value</title> <para> <returnvalue> Dateinamens-Endung: klein geschrieben, ohne Punkt (".") am Anfang. Beispiel: "xml" </returnvalue> </para> </refsect3> </refsect2> </refsect1> </refentry>

Diese Fragmente kann man in einen Abschnitt des Appendix einhängen.

Aktuelle supported Features

  • Javadoc-Kommentare für Klassen
    • Beschreibung
      • Autor
  • Javadoc-Kommentare für Methoden
    • Beschreibung
      • Parameter
      • Exceptions
      • Rückgabewerte

Alle Artikel rss Wochenübersicht Monatsübersicht Codeberg Repositories Mastodon Über mich home xmpp


Vor 5 Jahren hier im Blog

  • OAuth und OTP

    16.02.2020

    Wie bereits beschrieben will ich mich demnächst näher mit OAuth befassen...

    Weiterlesen...

Neueste Artikel

  • Split von Filesets in Apache ANT

    Ich musste neulich darüber nachdenken, eine Parallelisierung für einen meiner ANT-Tasks in meinem Static Site Generator einzubauen.

    Weiterlesen
  • Motion JPEG Erzeugung aus Java heraus

    Da ich mich in den letzten Wochen wieder einmal mit Javas Sicherheitsmechanismen und dem Erzeugen von Animationen beschäftigt habe, habe ich den Entschluss gefasst, die bisher mittels JMF AVIs in dWb+ zu erstetzen - nur wodurch?

    Weiterlesen
  • Animationen mit Multi-Scale Truchet Patterns

    Ich hatte neulich hier einen Link zu Multi-Scale Truchet Patterns und habe seitdem den Algorithmus mit java umgesetzt und ihn als Teil meines Projekts zur Testdatengenerierung veröffentlicht.

    Weiterlesen

Manche nennen es Blog, manche Web-Seite - ich schreibe hier hin und wieder über meine Erlebnisse, Rückschläge und Erleuchtungen bei meinen Hobbies.

Wer daran teilhaben und eventuell sogar davon profitieren möchte, muss damit leben, daß ich hin und wieder kleine Ausflüge in Bereiche mache, die nichts mit IT, Administration oder Softwareentwicklung zu tun haben.

Ich wünsche allen Lesern viel Spaß und hin und wieder einen kleinen AHA!-Effekt...

PS: Meine öffentlichen Codeberg-Repositories findet man hier.