001/*
002 * JDrupes MDoclet
003 * Copyright (C) 2021 Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU Affero General Public License as published by
007 * the Free Software Foundation; either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
013 * for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License along
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.taglets.plantUml;
020
021import java.io.IOException;
022import java.nio.file.Files;
023import java.nio.file.Paths;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.List;
027import java.util.Set;
028import java.util.logging.Level;
029import java.util.logging.Logger;
030
031import javax.lang.model.element.Element;
032import javax.lang.model.element.Name;
033import javax.lang.model.element.PackageElement;
034import javax.lang.model.element.TypeElement;
035import javax.lang.model.util.SimpleElementVisitor14;
036import javax.tools.DocumentationTool;
037import javax.tools.FileObject;
038import javax.tools.JavaFileManager;
039
040import com.sun.source.doctree.DocTree;
041
042import jdk.javadoc.doclet.Doclet;
043import jdk.javadoc.doclet.DocletEnvironment;
044import jdk.javadoc.doclet.Taglet;
045import net.sourceforge.plantuml.FileFormat;
046import net.sourceforge.plantuml.FileFormatOption;
047import net.sourceforge.plantuml.SourceStringReader;
048import net.sourceforge.plantuml.preproc.Defines;
049
050/**
051 * A JDK11 doclet that generates UML diagrams from PlantUML
052 * specifications in the comment.
053 */
054public class PlantUml implements Taglet {
055
056    private static final Logger logger
057        = Logger.getLogger(PlantUml.class.getName());
058
059    private DocletEnvironment env;
060    private JavaFileManager fileManager;
061    private List<String> plantConfigData;
062
063    @Override
064    public void init(DocletEnvironment env, Doclet doclet) {
065        this.env = env;
066        fileManager = env.getJavaFileManager();
067    }
068
069    @Override
070    public String getName() {
071        return "plantUml";
072    }
073
074    @Override
075    public Set<Location> getAllowedLocations() {
076        return Set.of(Taglet.Location.OVERVIEW, Taglet.Location.PACKAGE,
077            Taglet.Location.TYPE);
078    }
079
080    @Override
081    public boolean isInlineTag() {
082        return true;
083    }
084
085    @Override
086    public boolean isBlockTag() {
087        return true;
088    }
089
090    @Override
091    public String toString(List<? extends DocTree> tags, Element element) {
092        for (DocTree tagTree : tags) {
093            processTag(tagTree, element);
094        }
095        return "";
096    }
097
098    private void processTag(DocTree tree, Element element) {
099        String plantUmlSource = tree.toString();
100        String[] splitSource = plantUmlSource.split("\\s", 3);
101        if (splitSource.length < 3) {
102            logger.log(Level.WARNING, "Invalid %0 tag. Content: %1",
103                new Object[] { getName(), tree.toString() });
104            throw new IllegalArgumentException("Invalid " + getName()
105                + " tag: Expected filename and PlantUML source");
106        }
107
108        String packageName = "";
109        String elementType = element.getClass().getName();
110        if (elementType.endsWith("Symbol$ClassSymbol")
111            || elementType.endsWith("Symbol$PackageSymbol")) {
112            packageName = extractPackageName(element);
113        }
114        FileObject graphicsFile;
115        try {
116            graphicsFile = fileManager.getFileForOutput(
117                DocumentationTool.Location.DOCUMENTATION_OUTPUT, packageName,
118                splitSource[1], null);
119        } catch (IOException e) {
120            throw new RuntimeException(
121                "Error generating output file for " + packageName + "/"
122                    + splitSource[1] + ": " + e.getLocalizedMessage());
123        }
124
125        // render
126        String content = splitSource[2].trim();
127        if (content.startsWith("<!--") && content.endsWith("-->")) {
128            content = content.substring(4, content.length() - 3);
129        }
130        plantUmlSource = "@startuml\n" + content + "\n@enduml";
131        SourceStringReader reader = new SourceStringReader(
132            Defines.createEmpty(), plantUmlSource, plantConfig());
133        try {
134            reader.outputImage(graphicsFile.openOutputStream(),
135                new FileFormatOption(getFileFormat(splitSource[1])));
136        } catch (IOException e) {
137            throw new RuntimeException(
138                "Error generating UML image " + graphicsFile.getName() + ": "
139                    + e.getLocalizedMessage());
140        }
141
142    }
143
144    private String extractPackageName(Element element) {
145        return element.accept(new SimpleElementVisitor14<>() {
146
147            @Override
148            public Name visitPackage(PackageElement e, Object p) {
149                return e.getQualifiedName();
150            }
151
152            @Override
153            public Name visitType(TypeElement e, Object p) {
154                return env.getElementUtils().getPackageOf(e).getQualifiedName();
155            }
156
157        }, null).toString();
158    }
159
160    private FileFormat getFileFormat(String name) {
161        for (FileFormat f : FileFormat.values()) {
162            if (name.toLowerCase().endsWith(f.getFileSuffix())) {
163                return f;
164            }
165        }
166        String msg = "Unsupported file extension: " + name;
167        throw new IllegalArgumentException(msg);
168    }
169
170    private List<String> plantConfig() {
171        if (plantConfigData != null) {
172            return plantConfigData;
173        }
174        plantConfigData = new ArrayList<>();
175        String configFileName
176            = System.getProperty(getClass().getName() + ".config");
177        if (configFileName == null) {
178            return plantConfigData;
179        }
180        try {
181            plantConfigData = Collections.singletonList(
182                new String(Files.readAllBytes(Paths.get(configFileName))));
183        } catch (IOException e) {
184            throw new RuntimeException(
185                "Error loading PlantUML configuration file "
186                    + configFileName + ": " + e.getLocalizedMessage());
187        }
188
189        return plantConfigData;
190    }
191
192}