Doclets

De ChuWiki


Qué es un Doclet en java[editar]

Un Doclet es la clase encargada de generar la documentación de nuestros fuentes java a partir de sus comentarios.

Cuando hacemos

javadoc fuentes.java

javadoc recoge la información en los fuentes de java y se la pasa a una clase java llamada StandardDoclet. Esta clase es la encargada de generar la documentación de javadoc tal cual la conocemos. Tienes un ejemplo de documentación generada por javadoc en javadoc de librería gráfica en java

Sin embargo, es fácil cambiar esta clase StandardDoclet por defecto por otra. Basta con hacerla, compilarla y luego llamar a javadoc asi

javadoc -doclet MiClase -docletpath PathDeMiClase fuentes.java

Implementar un Doclet[editar]

En versiones de java 11 o superior, para implementar un Doclet debemos implementar la interface jdk.javadoc.doclet.Doclet. En java 11 o anteriores debíamos hacerlo usando las clases del paquete com.sun.javadoc, que ha sido eliminado en java 17. Java 11 contiene ambos, aunque marca como obsoleto com.sun.javadoc

Vamos con la versión moderna. La interface jdk.javadoc.doclet.Doclet tiene varios métodos que explicamos a continuación.

Tienes el ejemplo completo en DocletExample.java

Método init()[editar]

javadoc llamará a este método en el inicio. No es necesario hacer nada especial, sólo lo que nuestro Doclet necesite.

    @Override
    public void init(Locale locale, Reporter reporter) {
        reporter.print(Diagnostic.Kind.NOTE, "Doclet using locale: " + locale);
        this.reporter = reporter;
    }

Nos pasa el Locale con el que se quiere generar la documentación javadoc y nos pasa una clase Reporter que sólo sirve para que escribamos en ella información opcional sobre qué está haciendo nuestro Doclet para mostrar al usuario cuando ejecute javadoc. En el ejemplo, simplemente mostramos qué Locale nos están pasando. El primer parámetro Diagnostic.Kind.NOTE es el tipo de información que estamos pasando. Tenemos opciones de ERROR, WARNING, etc.

Podemos dejar el método vacío si no queremos sacar este tipo de información.

Método run()[editar]

Este es el método al que javadoc llamará cuando quiera que se genere la documentación.

    @Override
    public boolean run(DocletEnvironment docEnv) {
        reporter.print(Diagnostic.Kind.NOTE, "myCustomOption: " + myCustomOption);

        DocTrees docTrees = docEnv.getDocTrees();
        final Set<? extends Element> specifiedElements = docEnv.getSpecifiedElements();
        specifiedElements.forEach(element -> {
            printEnclosedElements(docTrees, element);
        });

        return true;
    }

Nos pasa como parámetro un DocletEnvironment. De él podemos obtener todo lo necesario para saber qué paquetes, clases, métodos, comentarios, etc hay en los fuentes. A partir de esta información, podemos generar nuestra propia documentación o hacer el análisis que queramos de los fuentes.

Veamos algunos detalles

Acceder a paquetes, clases, métodos y atributos[editar]

  • docEnv.getDocTrees() nos devuelve un árbol con todos los comentarios de los fuentes java, separados por elementos (clase, método, parámetro, etc). Lo necesitaremos si queremos acceder a los comentarios en el código.
  • docEnv.getSpecifiedElements() Nos devuelve los elementos que se han especificado en la línea de comandos. Un elemento es una clase, un paquete, etc. Si miras el comando javadoc que pusimos al principio javadoc fuentes.java, el elemento sería la clase contenida en ese fuentes.java.
  • printEnclosedElements() es un método que hemos hecho para sacar por pantalla datos de los elementos que nos han pasado. Vemos los detalles.
    public void printEnclosedElements (DocTrees tree, Element e){
        System.out.println(e.getKind() +": "+e);
        printComments(tree, e);
        final List<? extends Element> enclosedElements = e.getEnclosedElements();
        enclosedElements.forEach(element -> {
            printEnclosedElements(tree, element);
        });
    }

Es un método recursivo, es decir, se llama a sí mismo.

Lo primero que hace, con System.out.println() es sacar por pantalla el elemento e que nos han pasado (su nombre) y su tipo e.getKind() (si es una clase, un método, un constructor, ...). Algunos ejemplos de salida de esta línea podrían ser

PACKAGE: com.chuidiang
CLASS: com.chuidiang.App
CONSTRUCTOR: App()
METHOD: main(java.lang.String[])
...

es decir, a lo largo de la ejecución, han llegado a esta línea elementos package (com.chuidiang), clases (com.chuidiang.App), contructores (App()), métodos (main(java.lan.String[])), etc.

La llamada a printComments() es otro método que nos hemos hecho. Sacaría por pantalla los comentarios en el código fuente del elemento, así como los "tag" en el comentario (cosas como @author, @deprecated, etc). Vemos este método más adelante.

Con e.getEnclosedElements() obtenemos los "hijos" del elemento en curso. Por ejemplo, si el elemento en curso es un paquete, obtendríamos las clases, si es una clase, sus métodos y atributos. Así que un bucle para recorrerlos y llamada recursiva al método printEnclosedElements().

Acceder a los comentarios[editar]

Veamos al parte de los comentarios

    public void printComments(DocTrees trees, Element e) {
        DocCommentTree docCommentTree = trees.getDocCommentTree(e);
        if (docCommentTree != null) {
            System.out.println("  body comment : " + docCommentTree.getFullBody());
            System.out.println("  tags : " + docCommentTree.getBlockTags());
        } else {
            System.out.println("  No comments");
        }
    }

Como hemos comentado, esta parte es para sacar por pantalla los comentarios en el código fuente.

  • trees.getDocCommentTree() devuelve los comentarios del elemento que se le pasa como parámetro.
  • docCommentTree.getFullBody() obtiene el texto del comentario.
  • docCommentTree.getBlockTags() obtiene las eqituetas (@autor, @deprectaed, etc) en el comentario.

Si nuestra clase App tiene el siguiente comentario

/**
 * Hello world!
 *
 * @author Chuidiang
 */
public class App 
{
   ...
}

La salida de este método sería similar a la siguiente

  body comment : Hello world!
  tags : @author Chuidiang

getName()[editar]

Tenemos que implementar el método getName(). Este es fácil, basta devolver el nombre que queramos para nuestro Doclet, sin espacios.

    @Override
    public String getName() {
        return "Example";
    }

getSupportedOptions()[editar]

Si queremos poder pasar algunas opciones propias a nuestro Doclet desde la línea de comandos, por ejemplo

javadoc -misopciones a b fuente.java

debemos implementar este método devolviendo un Set<Option> de opciones. La interface Option tiene unos cuantos métodos a implementar, aunque algunos son informativos. Ponemos el código y la explicación en el comentario

new Option() {
    /**
     * Una descripción de para qué sirve nuestra opción para enseñársela al usuario.
     * @return
     */
    @Override
    public String getDescription() {
        return "my custom option";
    }

    /**
     * Qué tipo de opcion. Hay STANDARD, EXTENDED y OTHER. Habitualmente usaremos
     * STANDARD
     * @return
     */
    @Override
    public Option.Kind getKind() {
        return Option.Kind.STANDARD;
    }

    /**
     * Debemos devolver todos los pasibles alias para nuestra opción. Por ejemplo
     * puede ser "-myCustomOption" o su forma abreviada "-co" o su forma expandida
     * "-my-custom-option"
     */
    @Override
    public List<String> getNames() {
        return Arrays.asList(
                "-myCustomOption", "-co", "-my-custom-option");
    }

    /**
     * Indicamos cuantos valores lleva detrás nuestra opción. Por ejemplo, si en
     * línea de comandos ponemos
     * javadoc -co valor1 valor2 valor3
     * y en este método devolvermos 1, solo nos pasarán valor1 como parte de nuestra opción.
     * Si ponemos 2, nos pasaraán valor1 y valor2, etc.
     * @return
     */
    @Override
    public int getArgumentCount() {
        return 1;
    }

    /**
     * Una descripción de qué tipo de valor esperamos para nuestra opción. Algo com "path de los
     * fuentes", "autor", "comentario". Es para mostrarle al usuario y que sepa qué tipo de dato
     * espera nuestra opción.
     * @return
     */
    @Override
    public String getParameters() {
        return "any text";
    }

    /**
     * Cuando se ejecuta el comando javadoc, nos pasarán aquí para cada una de nuestras
     * opciones los valores que el usuario ha escrito.
     * Debemos guardarlos para tenerlos disponibles y usarlos como hayamos previsto y
     * devolver true si está todo bien.
     * @param opt
     * @param arguments
     * @return
     */
    @Override
    public boolean process(String opt, List<String> arguments) {
        if (Arrays.asList(
                "-myCustomOption", "-co", "-my-custom-option").contains(opt)){
            myCustomOption = arguments.get(0);
            System.out.println("My Custom Option : "+myCustomOption);
        }
        return true;
    }
}

El método getSupportedOptions() de Doclet debe devolver un Set<Option> con todas las opciones que queramos que nuestro Doclet tenga.

Ejecución del Doclet[editar]

Debemos compilar nuestro Doclet y bien tener el .class, bien meterlo en un jar. Vamos con esta segunda opción. La ejecución sería como la siguiente

javadoc -doclet com.chuidiang.doclet.DocletExample -docletpath maven-doclet-1.0-SNAPSHOT.jar -co Pepe \\
    --source-path ..\..\maven-javadoc\src\main\java\ -subpackages com.chuidiang

Vamos por partes

  • -doclet para indicar el nombre de nuestra clase Doclet, incluyendo en qué paquete está.
  • -docletpath para indicar el jar donde está empaquetado nuestro Doclet
  • -co es la opción que dijimos que queríamos que se le pudiera pasar a nuestro Doclet. Recibiremos Pepe. Ya es cosa nuestra qué hagamos con esa opción y valor.
  • --source-path es una opción estándar de javadoc. Sirve para indicar el directorio de los fuentes de los que queremos sacar el javadoc.
  • --subpackages. También es una opción estándar de javadoc. Indica de qué paquetes queremos sacar javadoc, incluyendo subpaquetes. Estos son los elementos que nos pasarán a nuestro Doclet.

Ejemplo de salida[editar]

Si ejecutamos nuestro Doclet sobre una clase App de java con un Hola mundo, que esté comentada, tendermos algo como lo siguiente

Note: Doclet using locale: es_ES
My Custom Option : Pepe
Loading source files for package com.chuidiang...
Constructing Javadoc information...
Note: myCustomOption: Pepe
PACKAGE: com.chuidiang
  No comments
CLASS: com.chuidiang.App
  body comment : Hello world!
  tags : @author Chuidiang
CONSTRUCTOR: App()
  No comments
METHOD: main(java.lang.String[])
  body comment : El main
  tags : @param args argumentos

Doclets con Maven[editar]

Puedes utilizar tus doclet con maven-javadoc-plugin en maven.

Doclets de terceros[editar]

¿Y por qué no ir más allá?. Un Doclet no tiene porqué generar documentación. Con la información de los fuentes recibida puede hacer cualquier cosa. Hay Doclet también que revisan el estilo de código a ver si cumple las convenciones, que generan esqueletos de test para JUnit, que a partir de un bean java generan los EJBs necesarios, que generan ficheros html con los fuentes y la sintaxis coloreada, etc, etc, etc.

En http://www.doclet.com hay un montón de clases "Doclet" ya hechas que hacen montones de cosas, como generar la documentación añadiéndole un diagrama de clases automáticamente, generarla en pdf, xml o rtf, etc. También con opciones como las que acabamos de comentar que no son solo para generar documentación.


Enlaces[editar]