001/*
002 * JDrupes MDoclet
003 * Copyright 2013 Raffael Herzog
004 * Copyright (C) 2017 Michael N. Lipp
005 * 
006 * This program is free software; you can redistribute it and/or modify it 
007 * under the terms of the GNU General Public License as published by 
008 * the Free Software Foundation; either version 3 of the License, or 
009 * (at your option) any later version.
010 * 
011 * This program is distributed in the hope that it will be useful, but 
012 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
013 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
014 * for more details.
015 * 
016 * You should have received a copy of the GNU General Public License along 
017 * with this program; if not, see <http://www.gnu.org/licenses/>.
018 */
019package org.jdrupes.mdoclet;
020
021import java.io.File;
022import java.nio.charset.Charset;
023import java.nio.charset.IllegalCharsetNameException;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Set;
031import java.util.regex.Matcher;
032import java.util.regex.Pattern;
033
034import org.jdrupes.mdoclet.processors.FlexmarkProcessor;
035
036import com.sun.javadoc.DocErrorReporter;
037import com.sun.tools.doclets.standard.Standard;
038
039/**
040 * Processes and stores the command line options.
041 */
042public class Options {
043
044        public static final String OPT_MARKDOWN_PROCESSOR = "-markdown-processor";
045    public static final String OPT_DISABLE_HIGHLIGHT = "-disable-highlight";
046    public static final String OPT_DISABLE_AUTO_HIGHLIGHT = "-disable-auto-highlight";
047    public static final String OPT_HIGHLIGHT_STYLE = "-highlight-style";
048    public static final String OPT_ENCODING = "-encoding";
049    public static final String OPT_OVERVIEW = "-overview";
050    public static final String OPT_OUTPUT_DIR = "-d";
051    public static final String OPT_STYLESHEETFILE = "-stylesheetfile";
052    public static final String OPT_TAG = "-tag";
053
054    private static final Pattern OPT_TAG_PATTERN 
055        = Pattern.compile("(?<tag>.*?)(?<!\\\\):(?<flags>[^:]*)(:(?<title>.*))?");
056    
057    private File overviewFile = null;
058    private Charset encoding = null;
059    private File destinationDir = null;
060    private File stylesheetFile = null;
061    private boolean highlightEnabled = true;
062    private String highlightStyle = null;
063    private Set<String> markedDownTags = new HashSet<>();
064    
065    private static MarkdownProcessor processor = new FlexmarkProcessor();
066    private static boolean processorUsed = false;
067    private static List<String[]> processorOptions = new ArrayList<>();
068
069    public static int optionLength(String option) {
070        
071        if (option.startsWith("-M") || option.startsWith("-T")) {
072                return 1;
073        }
074                switch (option) {
075                case OPT_MARKDOWN_PROCESSOR:
076                case OPT_HIGHLIGHT_STYLE:
077                case OPT_DISABLE_HIGHLIGHT:
078                        return 2;
079                case OPT_DISABLE_AUTO_HIGHLIGHT:
080                        return processor.isSupportedOption
081                                        (MarkdownProcessor.INTERNAL_OPT_DISABLE_AUTO_HIGHLIGHT) + 1;
082                default:
083                        return Standard.optionLength(option);
084                }
085    }
086
087    /**
088     * As specified by the Doclet specification.
089     *
090     * @param options          The command line options.
091     * @param errorReporter    An error reporter to print messages.
092     *
093     * @return `true` if the options are valid.
094     *
095     * @see com.sun.javadoc.Doclet#validOptions(String[][], com.sun.javadoc.DocErrorReporter)
096     */
097    public static boolean validOptions(String[][] options, DocErrorReporter errorReporter) {
098        String[][] forwardedOptions = new Options().load(options, errorReporter);
099        if ( forwardedOptions != null ) {
100            return Standard.validOptions(options, errorReporter);
101        }
102        else {
103            return false;
104        }
105    }
106
107    /**
108     * Loads the options from the command line.
109     *
110     * @param options          The command line options.
111     * @param errorReporter    The error reporter for printing messages.
112     *
113     * @return The options to be forwarded to the standard doclet.
114     */
115    public String[][] load(String[][] options, DocErrorReporter errorReporter) {
116        LinkedList<String[]> optionsList = new LinkedList<>();
117        for (String[] opt: options) {
118                optionsList.add(Arrays.copyOf(opt, opt.length));
119        }
120        Iterator<String[]> optionsIter = optionsList.iterator();
121        while ( optionsIter.hasNext() ) {
122            if ( !handleOption(optionsIter, errorReporter) ) {
123                return null;
124            }
125        }
126        return optionsList.toArray(new String[optionsList.size()][]);
127    }
128
129    protected boolean handleOption(Iterator<String[]> optionsIter, DocErrorReporter errorReporter) {
130        String[] opt = optionsIter.next();
131        switch (opt[0]) {
132        
133        // Standard options that we need to know about (i.e. copy)
134        case OPT_ENCODING:
135            try {
136                encoding = Charset.forName(opt[1]);
137            }
138            catch ( IllegalCharsetNameException e ) {
139                errorReporter.printError("Illegal charset: " + opt[1]);
140                return false;
141            }
142            return true;
143
144        case OPT_OUTPUT_DIR:
145            if ( destinationDir != null ) {
146                errorReporter.printError(OPT_OUTPUT_DIR + " may only be specified once");
147            }
148            setDestinationDir(new File(opt[1]));
149            return true;
150
151        case OPT_STYLESHEETFILE:
152                if ( stylesheetFile != null ) {
153                        errorReporter.printError(OPT_STYLESHEETFILE + " may only specified once");
154            }
155                setStylesheetFile(new File(opt[1]));
156                return true;
157
158        case OPT_TAG:
159                Matcher matcher = OPT_TAG_PATTERN.matcher(opt[1]);
160                if (!matcher.matches()) {
161                        return true;
162                }
163                if (!matcher.group("flags").contains("M")) {
164                        return true;
165                }
166                markedDownTags.add(matcher.group("tag").replace("\\", ""));
167                opt[1] = matcher.group("tag") 
168                                + ":" + matcher.group("flags").replace("M", "");
169                if (matcher.group("title") != null) {
170                                opt[1] += ":" + matcher.group("title");
171                }
172                return true;
173                
174        // Standard options that we consume (i.e. don't forward)
175        case OPT_OVERVIEW:
176            if ( getOverviewFile() != null ) {
177                errorReporter.printError(OPT_OVERVIEW + " may only be specified once");
178                return false;
179            }
180            setOverviewFile(new File(opt[1]));
181            optionsIter.remove();
182            return true;
183            
184        // Our own options
185        case OPT_MARKDOWN_PROCESSOR:
186                if (processorUsed) {
187                        errorReporter.printError("Markdown processor cannot be changed"
188                                        + " after setting its options");
189                        return false;
190                }
191                MarkdownProcessor p = createProcessor(opt, errorReporter);
192                if (p == null) {
193                        return false;
194                }
195                processor = p;
196                return true;
197            
198        case OPT_DISABLE_HIGHLIGHT:
199                highlightEnabled = false;
200                optionsIter.remove();
201                return true;
202                
203        case OPT_DISABLE_AUTO_HIGHLIGHT:
204                if (processor.isSupportedOption
205                                (MarkdownProcessor.INTERNAL_OPT_DISABLE_AUTO_HIGHLIGHT) == -1) {
206                        return false;
207                }
208                processorOptions.add(new String[] 
209                                { MarkdownProcessor.INTERNAL_OPT_DISABLE_AUTO_HIGHLIGHT });
210                optionsIter.remove();
211                return true;
212                
213        case OPT_HIGHLIGHT_STYLE:
214                if ( highlightStyle != null ) {
215                        errorReporter.printError("Only one " + OPT_HIGHLIGHT_STYLE 
216                                        + " option allowed");
217                        return false;
218                }
219                highlightStyle = opt[1];
220                optionsIter.remove();
221
222        default:
223                break;
224        }
225
226        if (opt[0].startsWith("-M")) {
227                // Processor (type) may not be changed after providing options for it
228                processorUsed = true;
229                String[] pOpt = unpackOption(opt[0].substring(2));
230                if (processor.isSupportedOption(pOpt[0]) == -1) {
231                        return false;
232                }
233                processorOptions.add(pOpt);
234                optionsIter.remove();
235                return true;
236        }
237        
238        return true;
239    }
240
241    private static String[] unpackOption(String option) {
242        List<String> opts = new ArrayList<>();
243        String[] eSplit = option.split("=", 2);
244        opts.add(eSplit[0]);
245        if (eSplit.length > 1) {
246                String[] cSplit = eSplit[1].split("(?<!\\\\),");
247                Arrays.stream(cSplit).map(s -> s.replace("\\,", ","))
248                                .forEach(s -> opts.add(s));
249        }
250        return opts.toArray(new String[opts.size()]);
251    }
252    
253        private MarkdownProcessor createProcessor(String[] opt, DocErrorReporter errorReporter) {
254                try {
255                        @SuppressWarnings("unchecked")
256                        Class<MarkdownProcessor> mpc = (Class<MarkdownProcessor>) 
257                                        getClass().getClassLoader().loadClass(opt[1]);
258                        return (MarkdownProcessor)mpc.newInstance(); 
259                } catch (ClassNotFoundException | InstantiationException
260                                | IllegalAccessException | ClassCastException e) {
261                        errorReporter.printError("Markdown processor \"" + opt[1] 
262                                        + "\" cannot be loaded (" + e.getMessage()
263                                        + "), check name and docletpath");
264                        return null;
265                }
266        }
267
268    /**
269     * Returns the processor selected by the options.
270     * 
271     * @return the processor
272     */
273    public MarkdownProcessor getProcessor() {
274        return processor;
275    }
276
277    /**
278     * Returns the markdown processor options filtered out in
279     * {@link #load(String[][], DocErrorReporter)}.
280     * 
281     * @return the options
282     */
283    public String[][] getProcessorOptions() {
284        return processorOptions.toArray(new String[processorOptions.size()][]);
285    }
286
287    /**
288     * Returns the names of the tags that have been seen in the `-tag` options
289     * with an "`M`" flag.
290     * 
291     * @return the tags
292     */
293    public String[] getMarkedDownTags() {
294        return markedDownTags.toArray(new String[markedDownTags.size()]);
295    }
296    
297    /**
298     * Gets the overview file.
299     *
300     * @return The overview file.
301     */
302    public File getOverviewFile() {
303        return overviewFile;
304    }
305
306    /**
307     * Sets the overview file.
308     *
309     * @param overviewFile The overview file.
310     */
311    public void setOverviewFile(File overviewFile) {
312        this.overviewFile = overviewFile;
313    }
314
315    /**
316     * Gets the source encoding.
317     *
318     * @return The source encoding.
319     */
320    public Charset getEncoding() {
321        return encoding != null ? encoding : Charset.defaultCharset();
322    }
323
324    /**
325     * Sets the source encoding.
326     *
327     * @param encoding The source encoding.
328     */
329    public void setEncoding(Charset encoding) {
330        this.encoding = encoding;
331    }
332
333    /**
334     * Gets the destination directory.
335     *
336     * @return The destination directory.
337     */
338    public File getDestinationDir() {
339        if ( destinationDir == null ) {
340            destinationDir = new File(System.getProperty("user.dir"));
341        }
342        return destinationDir;
343    }
344
345    /**
346     * Sets the destination directory.
347     *
348     * @param destinationDir    The destination directory
349     */
350    public void setDestinationDir(File destinationDir) {
351        this.destinationDir = destinationDir;
352    }
353
354    /**
355     * Gets the CSS stylesheet file.
356     *
357     * @return The stylesheet file.
358     */
359    public File getStylesheetFile() {
360        return stylesheetFile;
361    }
362
363    /**
364     * Sets the CSS stylesheet file.
365     *
366     * @param stylesheetFile The stylesheet file.
367     */
368    public void setStylesheetFile(File stylesheetFile) {
369        this.stylesheetFile = stylesheetFile;
370    }
371
372    public boolean isHighlightEnabled() {
373        return highlightEnabled;
374    }
375
376    public void setHighlightEnabled(boolean highlightEnabled) {
377        this.highlightEnabled = highlightEnabled;
378    }
379
380    public String getHighlightStyle() {
381        return highlightStyle != null ? highlightStyle : "default";
382    }
383
384    public void setHighlightStyle(String highlightStyle) {
385        this.highlightStyle = highlightStyle;
386    }
387
388}