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}