001/*
002 * JDrupes PlantUML Taglet
003 * Copyright (C) 2017  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 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 General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018package org.jdrupes.taglets.plantUml;
019
020import java.io.File;
021import java.io.FileOutputStream;
022import java.io.IOException;
023import java.nio.charset.Charset;
024import java.nio.file.Files;
025import java.nio.file.Paths;
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.List;
029
030import com.sun.javadoc.Doc;
031import com.sun.javadoc.PackageDoc;
032import com.sun.javadoc.ProgramElementDoc;
033import com.sun.javadoc.RootDoc;
034import com.sun.javadoc.Tag;
035import com.sun.tools.doclets.formats.html.markup.RawHtml;
036import com.sun.tools.doclets.internal.toolkit.Configuration;
037import com.sun.tools.doclets.internal.toolkit.Content;
038import com.sun.tools.doclets.internal.toolkit.taglets.TagletWriter;
039
040import net.sourceforge.plantuml.FileFormat;
041import net.sourceforge.plantuml.FileFormatOption;
042import net.sourceforge.plantuml.SourceStringReader;
043import net.sourceforge.plantuml.preproc.Defines;
044
045/**
046 * A JDK8 doclet that generates UML diagrams from PlantUML specifications in the comment.
047 */
048public class PlantUml implements com.sun.tools.doclets.internal.toolkit.taglets.Taglet {
049
050        private List<String> plantConfigData = null; 
051        
052        private Charset getSourceEncoding(Configuration config) {
053                if (config.encoding == null || config.encoding.isEmpty()) {
054                        return Charset.defaultCharset();
055                }
056                return Charset.forName(config.encoding);
057        }
058        
059        private List<String> plantConfig(Configuration config) {
060                if (plantConfigData != null) {
061                        return plantConfigData;
062                }
063                plantConfigData = new ArrayList<>();
064                String configFileName = System.getProperty(getClass().getName() + ".config");
065                if (configFileName == null) {
066                        return plantConfigData;
067                }
068                try {
069                        plantConfigData = Collections.singletonList
070                                        (new String(Files.readAllBytes(Paths.get(configFileName)),
071                                         getSourceEncoding(config)));
072                } catch (IOException e) {
073                        config.root.printError("Error loading PlantUML configuration file "
074                                                + configFileName + ": " + e.getLocalizedMessage());
075                }
076                
077                return plantConfigData;
078        }
079        
080        /* (non-Javadoc)
081         * @see com.sun.tools.doclets.Taglet#getName()
082         */
083        @Override
084        public String getName() {
085                return "plantUml";
086        }
087
088        /* (non-Javadoc)
089         * @see com.sun.tools.doclets.Taglet#inConstructor()
090         */
091        @Override
092        public boolean inConstructor() {
093                return false;
094        }
095
096        /* (non-Javadoc)
097         * @see com.sun.tools.doclets.Taglet#inField()
098         */
099        @Override
100        public boolean inField() {
101                return false;
102        }
103
104        /* (non-Javadoc)
105         * @see com.sun.tools.doclets.Taglet#inMethod()
106         */
107        @Override
108        public boolean inMethod() {
109                return false;
110        }
111
112        /* (non-Javadoc)
113         * @see com.sun.tools.doclets.Taglet#inOverview()
114         */
115        @Override
116        public boolean inOverview() {
117                return true;
118        }
119
120        /* (non-Javadoc)
121         * @see com.sun.tools.doclets.Taglet#inPackage()
122         */
123        @Override
124        public boolean inPackage() {
125                return true;
126        }
127
128        /* (non-Javadoc)
129         * @see com.sun.tools.doclets.Taglet#inType()
130         */
131        @Override
132        public boolean inType() {
133                return true;
134        }
135
136        /* (non-Javadoc)
137         * @see com.sun.tools.doclets.Taglet#isInlineTag()
138         */
139        @Override
140        public boolean isInlineTag() {
141                return false;
142        }
143
144        @Override
145        public Content getTagletOutput(Tag tag, TagletWriter writer) 
146                        throws IllegalArgumentException {
147                return new RawHtml("");
148        }
149
150        @Override
151        public Content getTagletOutput(Doc holder, TagletWriter writer) 
152                        throws IllegalArgumentException {
153                Configuration config = writer.configuration();
154                for (Tag tag: holder.tags(getName())) {
155                        processTag(tag, config);
156                }
157                return new RawHtml("");
158        }
159
160        private void processTag(Tag tag, Configuration config) {                
161                // Get package name for generating output directory
162                String packageName;
163                if ( tag.holder() instanceof ProgramElementDoc ) {
164                        packageName = ((ProgramElementDoc)tag.holder()).containingPackage().name();
165                }
166                else if ( tag.holder() instanceof PackageDoc ) {
167                        packageName = ((PackageDoc)(tag.holder())).name();
168                }
169                else if ( tag.holder() instanceof RootDoc ) {
170                        packageName = null;
171                } else {
172                        config.root.printError
173                                (tag.position(), "Cannot handle tag for holder " + tag.holder());
174                        return;
175                }
176                String source = tag.text().trim();
177                String[] splitSource = source.split("\\s", 2);
178                if ( splitSource.length < 2 ) {
179                        config.root.printError
180                                (tag.position(), "Invalid " + getName() 
181                                + " tag: Expected filename and PlantUML source");
182          return;
183                }
184                FileFormat fileFormat = getFileFormat(config, splitSource[0]);
185                File outputFile = getOutputFile(config, packageName, splitSource[0]);
186                config.root.printNotice("Generating UML diagram " + outputFile);
187                // render
188                source = "@startuml\n" + splitSource[1].trim() + "\n@enduml";
189                SourceStringReader reader = new SourceStringReader
190                                (new Defines(), source, plantConfig(config));
191                try {
192                        reader.generateImage(new FileOutputStream(outputFile),
193                                        new FileFormatOption(fileFormat));
194                }
195                catch ( IOException e ) {
196                        config.root.printError
197                        (tag.position(), "Error generating UML image " + outputFile + ": " 
198                                        + e.getLocalizedMessage());
199                }
200        }
201
202        private FileFormat getFileFormat(Configuration config, String name) {
203                for (FileFormat f: FileFormat.values()) {
204                        if (name.toLowerCase().endsWith(f.getFileSuffix())) {
205                                return f;
206                        }
207                }
208                String msg = "Unsupported file extension: " + name;
209                config.root.printError(msg);
210                throw new IllegalArgumentException(msg);
211        }
212
213        private File getOutputFile(Configuration config, String packageName, String fileName) {
214                File outputDir = new File(config.destDirName);
215                if (packageName != null) {
216                        outputDir = new File(outputDir, packageName.replace(".", File.separator));
217                }
218                outputDir.mkdirs();
219                return new File(outputDir, fileName.replace("/", File.separator));
220        }
221        
222}