001package Torello.Java.Build;
002
003import Torello.Java.FileNode;
004import Torello.Java.FileRW;
005import Torello.Java.LFEC;
006import Torello.Java.Q;
007import Torello.Java.StrCmpr;
008import Torello.Java.StringParse;
009import Torello.Java.RTC;
010
011import static Torello.Java.C.*;
012
013import java.util.*;
014import java.util.regex.*;
015import java.io.*;
016
017import java.util.function.IntFunction;
018
019/**
020 * A very basic utility for ensuring that the Java Doc Comment portion of a source-file does not
021 * exceed a maximum line-length.
022 * 
023 * <BR /><BR /><EMBED CLASS='external-html' DATA-FILE-ID=LINT>
024 */
025public class Lint
026{
027    private Lint() { }
028
029
030    // ********************************************************************************************
031    // ********************************************************************************************
032    // MAIN COMMAND-LINE METHOD
033    // ********************************************************************************************
034    // ********************************************************************************************
035
036
037    private static final String man_page = '\n' +
038        "\tjava Torello.Java.Lint 1 <Java-Source-File>\n" +
039        "\tjava Torello.Java.Lint 1 INNERCLASS <Java-Source-File>\n" +
040        "\tjava Torello.Java.Lint 2 <external-html-directory> (replaces ' * ' and {@code })\n" +
041        "\tjava Torello.Java.Lint 3 <external-html-directory> (line-length checker)\n";
042
043    /**
044     * This class' operations are all performed via the command line.  It asks questions of the
045     * user from the command line, and lints the Java Doc Comments parts of {@code '.java'} files
046     * to ensure that no line comment line is longer than 100 characters.  This class is probably
047     * not too useful - <I>outside of the Java HTML Library!</I>  It did help me make all of my
048     * source-code files look nice when you click on the "Hi-Lited Source Code" links.
049     */
050    public static void main(String[] argv) throws IOException
051    {
052        if (    ((argv.length < 2) || (argv.length > 3))
053            ||  StrCmpr.equalsNAND(argv[0], "1", "2", "3")
054        )
055            System.out.println(man_page);
056
057        else if (argv[0].equals("1")) // Java Doc Linter
058        {
059            if (argv.length == 2) lint(argv[1]);
060            else
061            {
062                if (! argv[1].equals("INNERCLASS"))
063                {
064                    System.out.println(man_page);
065                    System.exit(0);
066                }
067
068                LONG_JD_COMMENT = LONG_JD_COMMENT_INNERCLASS;
069                COMMENT_LINE_BEGINNING = "    " + COMMENT_LINE_BEGINNING;
070                lint(argv[2]);
071            }
072        }
073
074        else if (argv[0].equals("2")) // External-HTML File Linter
075            externalHTML(argv[1]);
076
077        else if (argv[0].equals("3")) // External-HTML File Linter
078            lineLengthChecker(argv[1]);
079
080        else System.out.println(man_page);
081    }
082
083
084    // ********************************************************************************************
085    // ********************************************************************************************
086    // Java Doc Comments Linter
087    // ********************************************************************************************
088    // ********************************************************************************************
089
090
091    /** Regular Expression for breaking up long lines of JavaDoc Comments */
092    public static Pattern LONG_JD_COMMENT = Pattern.compile
093        ("^(     \\* [^\\n]{0,92})\\s([^\\n]+)\n");
094
095    /** Regular Expression for breaking up long lines of JavaDoc Comments */
096    public static final Pattern LONG_JD_COMMENT_INNERCLASS = Pattern.compile
097        ("^(         \\* [^\\n]{0,88})\\s([^\\n]+)\n");
098
099    /** Regular Expression for matching lines that have JavaDoc Upgrade HiLite Dividers */
100    public static final Pattern OPENING_HILITE_DIV = Pattern.compile(
101        "<DIV\\s+CLASS=['\\\"](EXAMPLE|SNIP|LOC|SHELL|HTML|REGEX|SHELLBLOCK|COMPLETE)" +
102        "(-SCROLL)?['\\\"]\\w*>\\w*\\{",
103        Pattern.CASE_INSENSITIVE);
104
105    /** Regular Expression for matching lines that have 'closing' HiLite Divider Elements. */
106    public static final Pattern CLOSING_HILITE_DIV = Pattern.compile
107        ("\\}\\w*<\\/DIV>", Pattern.CASE_INSENSITIVE);
108
109    /** Long lines that don't require being queried by the user, because they only have text */
110    public static final Pattern SAFE_ENDING = Pattern.compile
111        ("^.*?[\\w\\d\\s,\\-\\.]{20}$");
112
113    /** The first 7 characters of a line of text in JavaDoc Comments portions of source files. */
114    public static String COMMENT_LINE_BEGINNING = "     * ";
115
116    private static final String[] PREV_LINE_MUST_BE_STRS =
117    {
118        "* @",
119        "* <EMBED ",
120        "* <BR /><BR />",
121
122        "* <DIV ",
123        "* <BR /><DIV ",
124
125        "* <UL ",
126        "* <BR /><UL ",
127
128        "* <OL ",
129        "* <BR /><OL ",
130
131        "* <TABLE ",
132        "* <BR /><TABLE "
133    };
134
135    private static final String[] CANNOT_PREPEND_TO_LINE_STRS =
136        new String[11 + PREV_LINE_MUST_BE_STRS.length];
137    static
138    {
139        System.arraycopy(
140            PREV_LINE_MUST_BE_STRS, 0, CANNOT_PREPEND_TO_LINE_STRS,
141            11, PREV_LINE_MUST_BE_STRS.length
142        );
143
144        CANNOT_PREPEND_TO_LINE_STRS[0]  = "* </UL>";
145        CANNOT_PREPEND_TO_LINE_STRS[1]  = "* </OL>";
146        CANNOT_PREPEND_TO_LINE_STRS[2]  = "* </TABLE>";
147        CANNOT_PREPEND_TO_LINE_STRS[3]  = "* <LI>";
148        CANNOT_PREPEND_TO_LINE_STRS[4]  = "* </LI>";
149        CANNOT_PREPEND_TO_LINE_STRS[5]  = "* <TD>";
150        CANNOT_PREPEND_TO_LINE_STRS[6]  = "* </TD>";
151        CANNOT_PREPEND_TO_LINE_STRS[7]  = "* <TR>";
152        CANNOT_PREPEND_TO_LINE_STRS[8]  = "* </TR>";
153        CANNOT_PREPEND_TO_LINE_STRS[9]  = "* <TH>";
154        CANNOT_PREPEND_TO_LINE_STRS[10] = "* </TH>";
155    }
156
157    /**
158     * Performs a 'LINT' on the input Java Source Code File.
159     * 
160     * @param inFileOrDir This is any file or directory.  If this is a directory, the entire
161     * directory will be scanned for {@code '.java'} source-files.  If this is a file, then it
162     * will be the only file that is linted.
163     * 
164     * @throws FileNotFoundException If this file or directory is not found.
165     */
166    public static void lint(String inFileOrDir) throws IOException
167    {
168        File            f = new File(inFileOrDir);
169        Vector<String>  files;
170
171        if (! f.exists()) throw new FileNotFoundException
172            ("A file or directory named [" + inFileOrDir + "], was not found.");
173
174        boolean lintingDirectory = true;
175
176        if (f.isDirectory()) files = FileNode
177            .createRoot(inFileOrDir)
178            .loadTree(1, (File file, String name) -> name.endsWith(".java"), null)
179            .flattenJustFiles(RTC.FULLPATH_VECTOR());
180        else 
181        {
182            files = new Vector<>();
183            files.add(inFileOrDir);
184            lintingDirectory = false;
185        }
186
187        for (int fNum=0; fNum < files.size(); fNum++)
188        {
189            String file = files.elementAt(fNum);
190
191            System.out.println(
192                "Linting File [" + BCYAN + (fNum+1) + " of " + files.size() + RESET + "]\n" +
193                "Visiting File: " + BYELLOW + file + RESET + "\n"
194            );
195
196            Vector<String>  javaFile        = FileRW.loadFileToVector(file, true);
197            boolean         insideHiLiteDIV = false;
198            int             linesAdded      = 0;
199
200            for (int i=1; i < (javaFile.size()-1); i++)
201            {
202                String  line                    = javaFile.elementAt(i);
203                String  prevLine                = javaFile.elementAt(i-1);
204                String  nextLine                = javaFile.elementAt(i+1);
205
206                String  lineTrimmed             = line.trim();
207                String  prevLineTrimmed         = prevLine.trim();
208                String  nextLineTrimmed         = nextLine.trim();
209
210                boolean lineIsComment           = line.startsWith(COMMENT_LINE_BEGINNING);
211                boolean nextLineIsComment       = nextLine.startsWith(COMMENT_LINE_BEGINNING);
212                boolean prevLineWasComment      = prevLine.startsWith(COMMENT_LINE_BEGINNING);
213
214                boolean lineIsTooLong           = line.length() > 100;
215                boolean prevLineWasBlankComment = prevLineTrimmed.equals("*");
216                boolean nextLineIsEndingComment = nextLineTrimmed.equals("*/");
217                boolean nthSeeTagInARow         = lineTrimmed.startsWith("* @see") && 
218                                                    prevLineTrimmed.startsWith("* @see");
219
220                boolean hasOpeningHiLiteDIV     = lineIsComment && 
221                                                    OPENING_HILITE_DIV.matcher(lineTrimmed).find();
222
223                boolean hasClosingHiLiteDIV     = lineIsComment && 
224                                                    CLOSING_HILITE_DIV.matcher(lineTrimmed).find();
225
226                boolean mustBePreceededByBlankCommentLine =
227                    lineIsComment &&
228                    StrCmpr.startsWithXOR_CI(lineTrimmed, PREV_LINE_MUST_BE_STRS);
229
230                boolean appendToStartOfNextLineIsOK =
231                    nextLineIsComment &&
232                    (! StrCmpr.startsWithXOR_CI(nextLineTrimmed, CANNOT_PREPEND_TO_LINE_STRS)) &&
233                    (! nextLineIsEndingComment);
234
235                // ************************************************************
236                // MAIN LOOP PART
237                // ************************************************************
238
239                if (hasOpeningHiLiteDIV)
240                    insideHiLiteDIV = true;
241                else
242                {
243                    if (hasClosingHiLiteDIV)
244                    { insideHiLiteDIV = false; System.out.print(line); continue; }
245
246                    if (insideHiLiteDIV)
247                    { System.out.print(line); continue; }
248                } // tricky...
249
250                if (    mustBePreceededByBlankCommentLine
251                    &&  (! prevLineWasBlankComment)
252                    &&  (! nthSeeTagInARow)
253                    )
254                {
255                    linesAdded++;
256                    javaFile.add(i, COMMENT_LINE_BEGINNING + '\n');
257                    System.out.print(BGREEN + COMMENT_LINE_BEGINNING + RESET + '\n');
258                }
259                else if (lineIsComment && lineIsTooLong)
260                {
261                    System.out.print(BRED + line + RESET);
262                    Matcher m = LONG_JD_COMMENT.matcher(line);
263
264                    if (! m.find()) throw new IllegalStateException("MESSED UP, WTF?");
265
266                    String shortenedLine    = StringParse.trimRight(m.group(1));
267                    String restOfThisLine   = line.substring(m.end(1)).trim();
268
269                    System.out.print(shortenedLine + '\n');
270                    javaFile.setElementAt(shortenedLine + '\n', i);
271
272                    if (! SAFE_ENDING.matcher(shortenedLine).find())
273
274                        while (! Q.YN(
275                            "Break OK?  (Currently on Line #" + i + " of " +
276                            javaFile.size() + ")"
277                        ))
278                        {
279                            int pos = shortenedLine.lastIndexOf(' ');
280                            if (pos == -1) System.exit(0);
281
282                            restOfThisLine = shortenedLine.substring(pos + 1).trim() +
283                                ' ' + restOfThisLine;
284        
285                            shortenedLine = StringParse.trimRight(shortenedLine.substring(0, pos));
286
287                            System.out.print(BRED + shortenedLine + RESET + '\n');
288                            javaFile.setElementAt(shortenedLine + '\n', i);
289                        }
290
291                    if (restOfThisLine.length() == 0) continue;
292
293                    if (appendToStartOfNextLineIsOK)
294                    {
295                        nextLine = COMMENT_LINE_BEGINNING + restOfThisLine + ' ' + 
296                            nextLine.substring(COMMENT_LINE_BEGINNING.length());
297
298                        javaFile.setElementAt(nextLine, i+1);
299                    }
300                    else
301                    {
302                        linesAdded++;
303                        javaFile.add(i+1, COMMENT_LINE_BEGINNING + restOfThisLine + '\n');
304                    }
305                }
306                else System.out.print(line);
307            }
308
309            System.out.println(
310                "Finished File:\t" + BYELLOW + file + RESET + '\n' +
311                "Added [" + BCYAN + StringParse.zeroPad(linesAdded) + RESET + "] new lines " +
312                "to  the file."
313            );
314
315            for (int count=5; count > 0; count--) 
316                Q.YN(
317                    "Press Y/N " + BCYAN + StringParse.zeroPad(count) + RESET +
318                    " more times..."
319                );
320
321            String question = lintingDirectory
322                ? "Save and continue to next file?"
323                : "Save file?";
324
325            if (! Q.YN(GREEN + question + RESET)) System.exit(0);
326            FileRW.writeFile_NO_NEWLINE(javaFile, file);
327            System.out.println("Wrote File:\t" + BYELLOW + file + RESET);
328        }
329    }
330
331
332    // ********************************************************************************************
333    // ********************************************************************************************
334    // External HTML File Converter
335    // ********************************************************************************************
336    // ********************************************************************************************
337
338
339    private static final Pattern CODES = Pattern.compile("\\{@code [^\\}]+\\}");
340    private static final Pattern LINKS = Pattern.compile("\\{@link ([\\w.]+)?#?([^\\}]+)?\\}");
341    private static final Pattern JDSTARS = Pattern.compile("^\\s+\\*( |$)", Pattern.MULTILINE);
342
343    /**
344     * This can be a really great tool for transitioning a Source-File to use External-HTML Files.
345     * This method merely scans an HTML-File for items that need to be escaped or converted to 
346     * constructs that are usable in {@code '.html'} rather than {@code '.java'} files.
347     * 
348     * @param directoryName The name of a directory containing {@code '.html'} files.
349     */
350    public static void externalHTML(String directoryName) throws IOException
351    {
352        Iterator<String> externalHTMLFiles = FileNode
353            .createRoot(directoryName)
354            .loadTree(-1, (File dir, String fileName) -> fileName.endsWith(".html"), null)
355            .flattenJustFiles(RTC.FULLPATH_ITERATOR());
356
357        while (externalHTMLFiles.hasNext())
358        {
359            String  fileName    = externalHTMLFiles.next();
360            String  fileAsStr   = FileRW.loadFileToString(fileName);
361            String  newFileStr  = fileAsStr;
362            Matcher codes       = CODES.matcher(fileAsStr);
363
364            System.out.println("Linting File: " + BYELLOW + fileName + RESET);
365
366            if (! Q.YN("Shall We Lint?"))
367            {
368                if (! Q.YN("Continue to Next File? (say 'no' to Exit)")) System.exit(0);
369                else continue;
370            }
371
372
373
374            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
375            // Replace the {@code ...}
376            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
377
378            while (codes.find())
379            {
380                String s = codes.group();
381
382                String newS = 
383                    "<CODE>" +
384                    s.substring(7, s.length() - 1).replace("<", "&lt;").replace(">", "&gt;") +
385                    "</CODE>";
386
387                System.out.println(
388                    BYELLOW + s.replace("\n", "\\n") +  RESET +
389                    BRED + "\n    ===>\n" + RESET +
390                    BGREEN + newS + RESET
391                );
392
393                if (Q.YN("Replace this one?"))
394                {
395                    newFileStr = codes.replaceFirst(newS);
396                    codes.reset(newFileStr);
397                }
398            }
399
400
401            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
402            // Replace the {@link ...}  
403            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
404            //
405            // **NOTE: The replacements are JUST HELPERS, THEY ARE NOT COMPLETE.
406            //         This does about 95% of the typing for you, but it is not 100%
407            //         The relative-path string (dot-dots) has not been added.
408
409            Matcher links = LINKS.matcher(newFileStr);
410
411            while (links.find())
412            {
413                String s = links.group();
414
415                String file = links.group(1);
416                String rel = links.group(2);
417
418                String href = 
419                    ((file != null) ? (file + ".html") : "") +
420                    ((rel != null) ? ("#" + rel) : "");
421
422                String linkText =
423                    ((file != null) ? (file) : "") +
424                    ((rel != null)
425                        ? (((file != null) ? "." : "") + rel)
426                        : "");
427
428                String newS =
429                    "<B><CODE><A HREF='" + href + "'>" + linkText + "</CODE></B></A>";
430
431                System.out.println(
432                    BYELLOW + s.replace("\n", "\\n") +  RESET +
433                    BRED + "\n    ===>\n" + RESET +
434                    BGREEN + newS + RESET
435                );
436
437                if (Q.YN("Replace this one?"))
438                {
439                    newFileStr = links.replaceFirst("\n\n" + newS + "\n\n");
440                    links.reset(newFileStr);
441                }
442            }
443
444
445            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
446            // Replace the leading  "     * "
447            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
448
449            newFileStr = JDSTARS.matcher(newFileStr).replaceAll("");
450
451
452            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
453            // Save the file
454            // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
455
456            System.out.println(
457                newFileStr +
458                "\n************************************************************\n"
459            );
460
461            if (Q.YN("Save this File?"))
462            {
463                FileRW.writeFile(newFileStr, fileName);
464                System.out.println("Saved File: " + BYELLOW + fileName + RESET);
465            }           
466        }
467    }
468
469
470    // ********************************************************************************************
471    // ********************************************************************************************
472    // CHECKS FOR '.java' FILES WITH LONG LINE LENGTHS
473    // ********************************************************************************************
474    // ********************************************************************************************
475
476
477    /**
478     * This method will scan an entire directory for {@code '.java'} files, and then report if
479     * there are any lines in each of those files whose length is greater than 100.
480     * 
481     * <BR /><BR />While not exactly a great tool for all developers, during the development of
482     * Java HTML, this has been (on occasion) extremely useful.
483     * 
484     * @param directoryName The name of the directory to be scanned for {@code '.java'} files.
485     */
486    public static void lineLengthChecker(String directoryName) throws IOException
487    {
488        Iterator<String> javaFiles = FileNode
489            .createRoot(directoryName)
490            .loadTree(-1, (File dir, String fileName) -> fileName.endsWith(".java"), null)
491            .flattenJustFiles(RTC.FULLPATH_ITERATOR());
492
493        while (javaFiles.hasNext())
494        {
495            String              fileName    = javaFiles.next();
496            Vector<String>      lines       = FileRW.loadFileToVector(fileName, true);
497            boolean             hadMatch    = false;
498            IntFunction<String> zeroPad     = (lines.size() < 999)
499                                                ? StringParse::zeroPad
500                                                : StringParse::zeroPad10e4;
501
502            System.out.println("Checking File: " + BYELLOW + fileName + RESET);
503
504            for (int i=0; i < lines.size(); i++)
505
506                if (lines.elementAt(i).length() > 100)
507                {
508                    String l = lines.elementAt(i);
509
510                    System.out.print(
511                        "Line-Number: " +
512
513                        // NOTE: add one to the line number, when printing it.  The lines
514                        //       vector index starts at zero, not one.
515
516                        '[' + BGREEN + zeroPad.apply(i + 1) + RESET + ", " +
517                        "Has Length: " + BRED + l.length() + RESET + '\n' +
518
519                        // Print the line
520                        l
521                    );
522
523                    hadMatch = true;
524                }
525
526            if (hadMatch) if (! Q.YN("continue?")) System.exit(0);
527        }
528    }
529}