View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.jxr;
20  
21  /*
22   * CodeViewer.java
23   * CoolServlets.com
24   * March 2000
25   *
26   * Copyright (C) 2000 CoolServlets.com
27   *
28   * Redistribution and use in source and binary forms, with or without
29   * modification, are permitted provided that the following conditions are met:
30   * 1) Redistributions of source code must retain the above copyright notice,
31   *   this list of conditions and the following disclaimer.
32   * 2) Redistributions in binary form must reproduce the above copyright notice,
33   *   this list of conditions and the following disclaimer in the documentation
34   *   and/or other materials provided with the distribution.
35   * 3) Neither the name CoolServlets.com nor the names of its contributors may be
36   *   used to endorse or promote products derived from this software without
37   *   specific prior written permission.
38   *
39   * THIS SOFTWARE IS PROVIDED BY COOLSERVLETS.COM AND CONTRIBUTORS ``AS IS'' AND
40   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
41   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
42   * DISCLAIMED. IN NO EVENT SHALL COOLSERVLETS.COM OR CONTRIBUTORS BE LIABLE FOR
43   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
44   * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
45   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
46   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
47   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
48   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
49   */
50  
51  import java.io.BufferedReader;
52  import java.io.FileInputStream;
53  import java.io.FileOutputStream;
54  import java.io.FileReader;
55  import java.io.FileWriter;
56  import java.io.IOException;
57  import java.io.InputStreamReader;
58  import java.io.ObjectInputStream;
59  import java.io.ObjectOutputStream;
60  import java.io.OutputStreamWriter;
61  import java.io.PrintWriter;
62  import java.io.Reader;
63  import java.io.Serializable;
64  import java.io.Writer;
65  import java.nio.file.Files;
66  import java.nio.file.Path;
67  import java.util.Collections;
68  import java.util.HashSet;
69  import java.util.Hashtable;
70  import java.util.List;
71  import java.util.Locale;
72  import java.util.Map;
73  import java.util.Set;
74  
75  import org.apache.maven.jxr.pacman.ClassType;
76  import org.apache.maven.jxr.pacman.FileManager;
77  import org.apache.maven.jxr.pacman.ImportType;
78  import org.apache.maven.jxr.pacman.JavaFile;
79  import org.apache.maven.jxr.pacman.PackageManager;
80  import org.apache.maven.jxr.pacman.PackageType;
81  import org.apache.maven.jxr.util.SimpleWordTokenizer;
82  import org.apache.maven.jxr.util.StringEntry;
83  
84  /**
85   * Syntax highlights java by turning it into HTML. A codeviewer object is created and then keeps state as lines are
86   * passed in. Each line passed in as java test, is returned as syntax highlighted HTML text. Users of the class can set
87   * how the java code will be highlighted with setter methods. Only valid java lines should be passed in since the object
88   * maintains state and may not handle illegal code gracefully. The actual system is implemented as a series of filters
89   * that deal with specific portions of the java code. The filters are as follows:
90   *
91   * <pre>
92   *  htmlFilter
93   *    |__
94   *      ongoingMultiLineCommentFilter -&gt; uriFilter
95   *        |__
96   *          inlineCommentFilter
97   *            |__
98   *              beginMultiLineCommentFilter -&gt; ongoingMultiLineCommentFilter
99   *                |__
100  *                  stringFilter
101  *                    |__
102  *                      keywordFilter
103  *                        |__
104  *                          uriFilter
105  *                            |__
106  *                              jxrFilter
107  *                                |__
108  *                                  importFilter
109  * </pre>
110  */
111 public class JavaCodeTransform implements Serializable {
112     // ----------------------------------------------------------------------
113     // public fields
114     // ----------------------------------------------------------------------
115 
116     /**
117      * show line numbers
118      */
119     private static final boolean LINE_NUMBERS = true;
120 
121     /**
122      * start comment delimiter
123      */
124     private static final String COMMENT_START = "<em class=\"jxr_comment\">";
125 
126     /**
127      * end comment delimiter
128      */
129     private static final String COMMENT_END = "</em>";
130 
131     /**
132      * start javadoc comment delimiter
133      */
134     private static final String JAVADOC_COMMENT_START = "<em class=\"jxr_javadoccomment\">";
135 
136     /**
137      * end javadoc comment delimiter
138      */
139     private static final String JAVADOC_COMMENT_END = "</em>";
140 
141     /**
142      * start String delimiter
143      */
144     private static final String STRING_START = "<span class=\"jxr_string\">";
145 
146     /**
147      * end String delimiter
148      */
149     private static final String STRING_END = "</span>";
150 
151     /**
152      * start reserved word delimiter
153      */
154     private static final String RESERVED_WORD_START = "<strong class=\"jxr_keyword\">";
155 
156     /**
157      * end reserved word delimiter
158      */
159     private static final String RESERVED_WORD_END = "</strong>";
160 
161     /**
162      * stylesheet file name
163      */
164     private static final String STYLESHEET_FILENAME = "stylesheet.css";
165 
166     private static final String[] VALID_URI_SCHEMES = {"http://", "https://", "mailto:"};
167 
168     /**
169      * Specify the only characters that are allowed in a URI besides alpha and numeric characters. Refer RFC2396 -
170      * http://www.ietf.org/rfc/rfc2396.txt
171      */
172     private static final char[] VALID_URI_CHARS = {
173         '?', '+', '%', '&', ':', '/', '.', '@', '_', ';', '=', '$', ',', '-', '!', '~', '*', '\'', '(', ')'
174     };
175 
176     // ----------------------------------------------------------------------
177     // private fields
178     // ----------------------------------------------------------------------
179 
180     /**
181      * HashTable containing java reserved words
182      */
183     private Map<String, String> reservedWords = new Hashtable<>();
184 
185     /**
186      * Flag set to true when a multi-line comment is started.
187      */
188     private boolean inMultiLineComment = false;
189 
190     /**
191      * Flag set to true when a javadoc comment is started.
192      */
193     private boolean inJavadocComment = false;
194 
195     /**
196      * File name that is currently being processed.
197      */
198     private Path currentFilename = null;
199 
200     /**
201      * Revision of the currently transformed document.
202      */
203     private String revision = null;
204 
205     /**
206      * The output encoding
207      */
208     private String outputEncoding = null;
209 
210     /**
211      * The wanted locale
212      */
213     private Locale locale = null;
214 
215     /**
216      * Relative path to javadocs, suitable for hyperlinking.
217      */
218     private Path javadocLinkDir;
219 
220     /**
221      * Package Manager for this project.
222      */
223     private final PackageManager packageManager;
224 
225     /**
226      * current file manager
227      */
228     private final FileManager fileManager;
229 
230     {
231         reservedWords.put("abstract", "abstract");
232         reservedWords.put("do", "do");
233         reservedWords.put("inner", "inner");
234         reservedWords.put("public", "public");
235         reservedWords.put("var", "var");
236         reservedWords.put("boolean", "boolean");
237         reservedWords.put("continue", "continue");
238         reservedWords.put("int", "int");
239         reservedWords.put("return", "return");
240         reservedWords.put("void", "void");
241         reservedWords.put("break", "break");
242         reservedWords.put("else", "else");
243         reservedWords.put("interface", "interface");
244         reservedWords.put("short", "short");
245         reservedWords.put("volatile", "volatile");
246         reservedWords.put("byvalue", "byvalue");
247         reservedWords.put("extends", "extends");
248         reservedWords.put("long", "long");
249         reservedWords.put("static", "static");
250         reservedWords.put("while", "while");
251         reservedWords.put("case", "case");
252         reservedWords.put("final", "final");
253         reservedWords.put("native", "native");
254         reservedWords.put("super", "super");
255         reservedWords.put("transient", "transient");
256         reservedWords.put("cast", "cast");
257         reservedWords.put("float", "float");
258         reservedWords.put("new", "new");
259         reservedWords.put("rest", "rest");
260         reservedWords.put("catch", "catch");
261         reservedWords.put("for", "for");
262         reservedWords.put("null", "null");
263         reservedWords.put("synchronized", "synchronized");
264         reservedWords.put("char", "char");
265         reservedWords.put("finally", "finally");
266         reservedWords.put("operator", "operator");
267         reservedWords.put("this", "this");
268         reservedWords.put("class", "class");
269         reservedWords.put("generic", "generic");
270         reservedWords.put("outer", "outer");
271         reservedWords.put("switch", "switch");
272         reservedWords.put("const", "const");
273         reservedWords.put("goto", "goto");
274         reservedWords.put("package", "package");
275         reservedWords.put("throw", "throw");
276         reservedWords.put("double", "double");
277         reservedWords.put("if", "if");
278         reservedWords.put("private", "private");
279         reservedWords.put("true", "true");
280         reservedWords.put("default", "default");
281         reservedWords.put("import", "import");
282         reservedWords.put("protected", "protected");
283         reservedWords.put("try", "try");
284         reservedWords.put("throws", "throws");
285         reservedWords.put("implements", "implements");
286     }
287 
288     public JavaCodeTransform(PackageManager packageManager, FileManager fileManager) {
289         this.packageManager = packageManager;
290         this.fileManager = fileManager;
291     }
292 
293     // ----------------------------------------------------------------------
294     // public methods
295     // ----------------------------------------------------------------------
296 
297     /**
298      * Now different method of seeing if at end of input stream, closes inputs stream at end.
299      *
300      * @param line String
301      * @return filtered line of code
302      */
303     private String syntaxHighlight(String line) {
304         return htmlFilter(line);
305     }
306 
307     /**
308      * Gets the header attribute of the JavaCodeTransform object
309      *
310      * @param out the writer where the header is appended to
311      */
312     private void appendHeader(PrintWriter out) {
313         String outputEncoding = this.outputEncoding;
314         if (outputEncoding == null) {
315             outputEncoding = "ISO-8859-1";
316         }
317 
318         // header
319         out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "
320                 + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
321         out.print("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"");
322         out.print(locale);
323         out.print("\" lang=\"");
324         out.print(locale);
325         out.println("\">");
326         out.print("<head>");
327         out.print("<meta http-equiv=\"content-type\" content=\"text/html; charset=");
328         out.print(outputEncoding);
329         out.println("\" />");
330 
331         // title ("classname xref")
332         out.print("<title>");
333         try {
334             JavaFile javaFile = fileManager.getFile(this.getCurrentFilename());
335             // Use the name of the file instead of the class to handle inner classes properly
336             if (javaFile.getClassType() != null && javaFile.getClassType().getFilename() != null) {
337                 out.print(javaFile.getClassType().getFilename());
338             } else {
339                 out.print(javaFile.getFilename());
340             }
341             out.print(' ');
342         } catch (IOException e) {
343             e.printStackTrace();
344         } finally {
345             out.println("xref</title>");
346         }
347 
348         // stylesheet link
349         out.print("<link type=\"text/css\" rel=\"stylesheet\" href=\"");
350         out.print(this.getPackageRoot());
351         out.print(STYLESHEET_FILENAME);
352         out.println("\" />");
353 
354         out.println("</head>");
355         out.println("<body>");
356         out.print(this.getFileOverview());
357 
358         // start code section
359         out.println("<pre>");
360     }
361 
362     /**
363      * Gets the footer attribute of the JavaCodeTransform object
364      *
365      * @param out the writer where the header is appended to
366      * @param bottom the bottom text
367      */
368     private void appendFooter(PrintWriter out, String bottom) {
369         out.println("</pre>");
370         out.println("<hr/>");
371         out.print("<div id=\"footer\">");
372         out.print(bottom);
373         out.println("</div>");
374         out.println("</body>");
375         out.println("</html>");
376     }
377 
378     /**
379      * This is the public method for doing all transforms of code.
380      *
381      * @param sourceReader Reader
382      * @param destWriter Writer
383      * @param locale String
384      * @param outputEncoding String
385      * @param javadocLinkDir String
386      * @param revision String
387      * @param bottom string
388      * @throws IOException
389      */
390     private void transform(
391             Reader sourceReader,
392             Writer destWriter,
393             Locale locale,
394             String outputEncoding,
395             Path javadocLinkDir,
396             String revision,
397             String bottom)
398             throws IOException {
399         this.locale = locale;
400         this.outputEncoding = outputEncoding;
401         this.javadocLinkDir = javadocLinkDir;
402         this.revision = revision;
403 
404         BufferedReader in = new BufferedReader(sourceReader);
405 
406         PrintWriter out = new PrintWriter(destWriter);
407 
408         String line;
409 
410         appendHeader(out);
411 
412         int linenumber = 1;
413         while ((line = in.readLine()) != null) {
414             if (LINE_NUMBERS) {
415                 out.print("<a class=\"jxr_linenumber\" name=\"L" + linenumber + "\" " + "href=\"#L" + linenumber + "\">"
416                         + linenumber + "</a>" + getLineWidth(linenumber));
417             }
418 
419             out.println(this.syntaxHighlight(line));
420 
421             ++linenumber;
422         }
423 
424         appendFooter(out, bottom);
425 
426         out.flush();
427     }
428 
429     /**
430      * This is the public method for doing all transforms of code.
431      *
432      * @param sourcefile source file
433      * @param destfile destination file
434      * @param locale locale
435      * @param inputEncoding input encoding
436      * @param outputEncoding output encoding
437      * @param javadocLinkDir relative path to javadocs
438      * @param revision revision of the module
439      * @param bottom bottom text
440      * @throws IOException in I/O failures in reading/writing files
441      */
442     public final void transform(
443             Path sourcefile,
444             Path destfile,
445             Locale locale,
446             String inputEncoding,
447             String outputEncoding,
448             Path javadocLinkDir,
449             String revision,
450             String bottom)
451             throws IOException {
452         this.setCurrentFilename(sourcefile);
453 
454         // make sure that the parent directories exist...
455         Files.createDirectories(destfile.getParent());
456 
457         try (Reader fr = getReader(sourcefile, inputEncoding);
458                 Writer fw = getWriter(destfile, outputEncoding)) {
459             transform(fr, fw, locale, outputEncoding, javadocLinkDir, revision, bottom);
460         } catch (RuntimeException e) {
461             System.out.println("Unable to processPath " + sourcefile + " => " + destfile);
462             throw e;
463         }
464     }
465 
466     private Writer getWriter(Path destfile, String outputEncoding) throws IOException {
467         Writer fw;
468         if (outputEncoding != null) {
469             fw = new OutputStreamWriter(new FileOutputStream(destfile.toFile()), outputEncoding);
470         } else {
471             fw = new FileWriter(destfile.toFile());
472         }
473         return fw;
474     }
475 
476     private Reader getReader(Path sourcefile, String inputEncoding) throws IOException {
477         Reader fr;
478         if (inputEncoding != null) {
479             fr = new InputStreamReader(new FileInputStream(sourcefile.toFile()), inputEncoding);
480         } else {
481             fr = new FileReader(sourcefile.toFile());
482         }
483         return fr;
484     }
485 
486     /**
487      * Gets the current file name.
488      *
489      * @return path of file
490      */
491     private Path getCurrentFilename() {
492         return this.currentFilename;
493     }
494 
495     /**
496      * Sets the current file name.
497      *
498      * @param filename file name
499      */
500     private void setCurrentFilename(Path filename) {
501         this.currentFilename = filename;
502     }
503 
504     /**
505      * From the current file, determine the package root based on the current path.
506      *
507      * @return package root
508      */
509     private String getPackageRoot() {
510         StringBuilder buff = new StringBuilder();
511 
512         JavaFile jf;
513 
514         try {
515             jf = fileManager.getFile(this.getCurrentFilename());
516         } catch (IOException e) {
517             e.printStackTrace();
518             return null;
519         }
520 
521         String current = jf.getPackageType().getName();
522 
523         int count = this.getPackageCount(current);
524 
525         for (int i = 0; i < count; ++i) {
526             buff.append("../");
527         }
528 
529         return buff.toString();
530     }
531 
532     /**
533      * Given a line of text, search for URIs and make href's out of them.
534      *
535      * @param line String
536      * @return href
537      */
538     private String uriFilter(String line) {
539         for (String scheme : VALID_URI_SCHEMES) {
540             int index = line.indexOf(scheme);
541 
542             if (index != -1) {
543                 int start = index;
544                 int end = -1;
545 
546                 for (int j = start; j < line.length(); ++j) {
547                     char current = line.charAt(j);
548 
549                     if (!Character.isLetterOrDigit(current) && isInvalidURICharacter(current)) {
550                         end = j;
551                         break;
552                     }
553 
554                     end = j;
555                 }
556 
557                 // now you should have the full URI so you can replace this
558                 // in the current buffer
559 
560                 if (end != -1) {
561                     String uri = (end + 1 == line.length()) ? line.substring(start) : line.substring(start, end);
562 
563                     line = line.replace(uri, "<a href=\"" + uri + "\" target=\"alexandria_uri\">" + uri + "</a>");
564                 }
565             }
566         }
567 
568         // if we are in a multiline comment we should not call JXR here.
569         if (!inMultiLineComment && !inJavadocComment) {
570             return jxrFilter(line);
571         }
572 
573         return line;
574     }
575 
576     /**
577      * The current revision of the module.
578      *
579      * @return revision
580      */
581     public final String getRevision() {
582         return this.revision;
583     }
584 
585     /**
586      * Cross Reference the given line with JXR returning the new content.
587      *
588      * @param line line
589      * @param packageName String
590      * @param classType ClassType
591      * @return cross-referenced line
592      */
593     private String xrLine(String line, String packageName, ClassType classType) {
594         StringBuilder buff = new StringBuilder(line);
595 
596         String link;
597         String find;
598         String href;
599 
600         if (classType != null) {
601             href = this.getHREF(packageName, classType);
602             find = classType.getName();
603 
604             // build out what the link would be.
605             link = "<a name=\"" + find + "\" href=\"" + href + "\">" + find + "</a>";
606         } else {
607             href = this.getHREF(packageName);
608             find = packageName;
609 
610             // build out what the link would be.
611             link = "<a href=\"" + href + "\">" + find + "</a>";
612         }
613 
614         // use the SimpleWordTokenizer to find all entries
615         // that match word. Then replace these with the link
616 
617         // now replace the word in the buffer with the link
618 
619         String replace = link;
620         List<StringEntry> tokens = SimpleWordTokenizer.tokenize(buff.toString(), find);
621 
622         // JXR-141: If there are more than 1 tokens to be replaced,
623         // then the start+end values are out of order during the
624         // buff.replace.
625         // Reversing the list solves it
626         Collections.reverse(tokens);
627 
628         for (StringEntry token : tokens) {
629             int start = token.getIndex();
630             int end = token.getIndex() + find.length();
631 
632             buff.replace(start, end, replace);
633         }
634 
635         return buff.toString();
636     }
637 
638     // ----------------------------------------------------------------------
639     // private methods
640     // ----------------------------------------------------------------------
641 
642     /**
643      * Filter HTML tags into more benign text.
644      *
645      * @param line String
646      * @return HTML-encoded line
647      */
648     private String htmlFilter(String line) {
649         if (line == null || line.equals("")) {
650             return "";
651         }
652         line = line.replace("&", "&amp;")
653                 .replace("<", "&lt;")
654                 .replace(">", "&gt;")
655                 .replace("\\\\", "&#92;&#92;")
656                 .replace("\\\"", "\\&quot;")
657                 .replace("'\"'", "'&quot;'");
658         return ongoingMultiLineCommentFilter(line);
659     }
660 
661     /**
662      * Handle ongoing multi-line comments, detecting ends if present.<br>
663      * State is maintained in private boolean members,
664      * one each for javadoc and (normal) multi-line comments.
665      *
666      * @param line line
667      * @return processed line
668      */
669     private String ongoingMultiLineCommentFilter(String line) {
670         if (line == null || line.equals("")) {
671             return "";
672         }
673         final String[] tags = inJavadocComment
674                 ? new String[] {JAVADOC_COMMENT_START, JAVADOC_COMMENT_END}
675                 : inMultiLineComment ? new String[] {COMMENT_START, COMMENT_END} : null;
676 
677         if (tags == null) {
678             // pass the line down to the next filter for processing.
679             return inlineCommentFilter(line);
680         }
681 
682         int index = line.indexOf("*/");
683         // only filter the portion without the end-of-comment,
684         // since * and / seem to be valid URI characters
685         String comment = uriFilter(index < 0 ? line : line.substring(0, index));
686         if (index >= 0) {
687             inJavadocComment = false;
688             inMultiLineComment = false;
689         }
690         StringBuilder buf = new StringBuilder(tags[0]).append(comment);
691 
692         if (index >= 0) {
693             buf.append("*/");
694         }
695         buf.append(tags[1]);
696 
697         if (index >= 0 && line.length() > index + 2) {
698             buf.append(inlineCommentFilter(line.substring(index + 2)));
699         }
700         return buf.toString();
701     }
702 
703     /**
704      * Filter inline comments from a line and formats them properly. One problem we'll have to solve here: comments
705      * contained in a string should be ignored... this is also true of the multi-line comments. So, we could either
706      * ignore the problem, or implement a function called something like isInsideString(line, index) where index points
707      * to some point in the line that we need to check... started doing this function below.
708      *
709      * @param line line
710      * @return processed line
711      */
712     private String inlineCommentFilter(String line) {
713         // assert !inJavadocComment;
714         // assert !inMultiLineComment;
715 
716         if (line == null || line.equals("")) {
717             return "";
718         }
719         int index = line.indexOf("//");
720         if ((index >= 0) && !isInsideString(line, index)) {
721             return beginMultiLineCommentFilter(line.substring(0, index))
722                     + COMMENT_START
723                     + line.substring(index)
724                     + COMMENT_END;
725         }
726 
727         return beginMultiLineCommentFilter(line);
728     }
729 
730     /**
731      * Detect and handle the start of multiLine comments. State is maintained in private boolean members one each for
732      * javadoc and (normal) multiline comments.
733      *
734      * @param line line
735      * @return processed line
736      */
737     private String beginMultiLineCommentFilter(String line) {
738         // assert !inJavadocComment;
739         // assert !inMultiLineComment;
740 
741         if (line == null || line.equals("")) {
742             return "";
743         }
744 
745         int index = line.indexOf("/*");
746         // check to see if a multi-line comment starts on this line:
747         if ((index > -1) && !isInsideString(line, index)) {
748             String fromIndex = line.substring(index);
749             if (fromIndex.startsWith("/**") && !(fromIndex.startsWith("/**/"))) {
750                 inJavadocComment = true;
751             } else {
752                 inMultiLineComment = true;
753             }
754             // Return result of other filters + everything after the start
755             // of the multiline comment. We need to pass the through the
756             // to the ongoing multiLineComment filter again in case the comment
757             // ends on the same line.
758             return stringFilter(line.substring(0, index)) + ongoingMultiLineCommentFilter(fromIndex);
759         }
760 
761         // Otherwise, no useful multi-line comment information was found so
762         // pass the line down to the next filter for processesing.
763         else {
764             return stringFilter(line);
765         }
766     }
767 
768     /**
769      * Filters strings from a line of text and formats them properly.
770      *
771      * @param line line
772      * @return processed line
773      */
774     private String stringFilter(String line) {
775         if (line == null || line.equals("")) {
776             return "";
777         }
778         StringBuilder buf = new StringBuilder();
779         if (line.indexOf('"') <= -1) {
780             return keywordFilter(line);
781         }
782         int start = 0;
783         int startStringIndex = -1;
784         int endStringIndex = -1;
785         int tempIndex;
786         // Keep moving through String characters until we want to stop...
787         while ((tempIndex = line.indexOf('"')) > -1) {
788             // We found the beginning of a string
789             if (startStringIndex == -1) {
790                 startStringIndex = 0;
791                 buf.append(stringFilter(line.substring(start, tempIndex)));
792                 buf.append(STRING_START).append('"');
793                 line = line.substring(tempIndex + 1);
794             }
795             // Must be at the end
796             else {
797                 startStringIndex = -1;
798                 endStringIndex = tempIndex;
799                 buf.append(line, 0, endStringIndex + 1);
800                 buf.append(STRING_END);
801                 line = line.substring(endStringIndex + 1);
802             }
803         }
804 
805         buf.append(keywordFilter(line));
806 
807         return buf.toString();
808     }
809 
810     /**
811      * Filters keywords from a line of text and formats them properly.
812      *
813      * @param line line
814      * @return processed line
815      */
816     private String keywordFilter(String line) {
817         final String classKeyword = "class";
818 
819         if (line == null || line.equals("")) {
820             return "";
821         }
822         StringBuilder buf = new StringBuilder();
823         int i = 0;
824         char ch;
825         StringBuilder temp = new StringBuilder();
826         while (i < line.length()) {
827             temp.setLength(0);
828             ch = line.charAt(i);
829             while (i < line.length() && ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))) {
830                 temp.append(ch);
831                 i++;
832                 if (i < line.length()) {
833                     ch = line.charAt(i);
834                 }
835             }
836             String tempString = temp.toString();
837 
838             // Special handling of css style class definitions
839             if (classKeyword.equals(tempString) && ch == '=') {
840                 i++;
841             } else if (reservedWords.containsKey(tempString)) {
842                 line = line.substring(0, i - tempString.length())
843                         + RESERVED_WORD_START
844                         + tempString
845                         + RESERVED_WORD_END
846                         + line.substring(i);
847                 i += (RESERVED_WORD_START.length() + RESERVED_WORD_END.length());
848             } else {
849                 i++;
850             }
851         }
852         buf.append(line);
853 
854         return uriFilter(buf.toString());
855     }
856 
857     /**
858      * Checks to see if some position in a line is between String start and ending characters. Not yet used in code or
859      * fully working :)
860      *
861      * @param line String
862      * @param position int
863      * @return boolean
864      */
865     private boolean isInsideString(String line, int position) {
866         if (line.indexOf('"') < 0) {
867             return false;
868         }
869         int index;
870         String left = line.substring(0, position);
871         String right = line.substring(position);
872         int leftCount = 0;
873         int rightCount = 0;
874         while ((index = left.indexOf('"')) > -1) {
875             leftCount++;
876             left = left.substring(index + 1);
877         }
878         while ((index = right.indexOf('"')) > -1) {
879             rightCount++;
880             right = right.substring(index + 1);
881         }
882         return (rightCount % 2 != 0 && leftCount % 2 != 0);
883     }
884 
885     /**
886      * @param oos ObjectOutputStream
887      * @throws IOException on I/O error during write
888      */
889     final void writeObject(ObjectOutputStream oos) throws IOException {
890         oos.defaultWriteObject();
891     }
892 
893     /**
894      * @param ois object input stream
895      * @throws ClassNotFoundException if the class of a serialized object could not be found.
896      * @throws IOException on I/O error during read
897      */
898     final void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
899         ois.defaultReadObject();
900     }
901 
902     /**
903      * Gets an overview header for this file.
904      *
905      * @return overview header
906      */
907     private String getFileOverview() {
908         StringBuilder overview = new StringBuilder();
909 
910         // only add the header if javadocs are present
911         if (javadocLinkDir != null) {
912             overview.append("<div id=\"overview\">");
913             // get the URI to get Javadoc info.
914             Path javadocURI;
915 
916             try {
917                 JavaFile jf = fileManager.getFile(this.getCurrentFilename());
918 
919                 javadocURI =
920                         javadocLinkDir.resolve(jf.getPackageType().getName().replace('.', '/'));
921                 // Use the name of the file instead of the class to handle inner classes properly
922                 String fileName;
923                 if (jf.getClassType() != null && jf.getClassType().getFilename() != null) {
924                     fileName = jf.getClassType().getFilename();
925                 } else {
926                     fileName = jf.getFilename();
927                 }
928                 javadocURI = javadocURI.resolve(fileName + ".html");
929 
930                 String javadocHREF = "<a href=\"" + javadocURI.toString().replace('\\', '/') + "\">View Javadoc</a>";
931 
932                 // get the generation time...
933                 overview.append(javadocHREF);
934             } catch (IOException e) {
935                 e.printStackTrace();
936             }
937 
938             overview.append("</div>");
939         }
940 
941         return overview.toString();
942     }
943 
944     /**
945      * Handles line width which may need to change depending on which line number you are on.
946      *
947      * @param linenumber int
948      * @return blanks
949      */
950     private String getLineWidth(int linenumber) {
951         if (linenumber < 10) {
952             return "   ";
953         } else if (linenumber < 100) {
954             return "  ";
955         } else {
956             return " ";
957         }
958     }
959 
960     /**
961      * Handles finding classes based on the current filename and then makes HREFs for you to link to them with.
962      *
963      * @param line line
964      * @return processed line
965      */
966     private String jxrFilter(String line) {
967         JavaFile jf;
968 
969         try {
970             // if the current file isn't set then just return
971             if (this.getCurrentFilename() == null) {
972                 return line;
973             }
974 
975             jf = fileManager.getFile(this.getCurrentFilename());
976         } catch (IOException e) {
977             e.printStackTrace();
978             return line;
979         }
980 
981         Set<String> packages = new HashSet<>();
982 
983         // get the imported packages
984         for (ImportType importType : jf.getImportTypes()) {
985             packages.add(importType.getPackage());
986         }
987 
988         // add the current package.
989         packages.add(jf.getPackageType().getName());
990 
991         List<StringEntry> words = SimpleWordTokenizer.tokenize(line);
992 
993         // go through each word and then match them to the correct class if necessary.
994         for (StringEntry word : words) {
995             for (String pkg : packages) {
996                 // get the package from the PackageManager because this will hold
997                 // the version with the classes also.
998 
999                 PackageType currentImport = packageManager.getPackageType(pkg);
1000 
1001                 // the package here might in fact be null because it wasn't parsed out
1002                 // this might be something that is either not included or is part
1003                 // of another package and wasn't parsed out.
1004 
1005                 if (currentImport == null) {
1006                     continue;
1007                 }
1008 
1009                 // see if the current word is within the package
1010 
1011                 // at this point the word could be a fully qualified package name
1012                 // (FQPN) or an imported package name.
1013 
1014                 String wordName = word.toString();
1015 
1016                 if (wordName.indexOf('.') != -1) {
1017                     // if there is a "." in the string then we have to assume
1018                     // it is a package.
1019 
1020                     String fqpnPackage = wordName.substring(0, wordName.lastIndexOf('.'));
1021                     String fqpnClass = wordName.substring(wordName.lastIndexOf('.') + 1);
1022 
1023                     // note. since this is a reference to a full package then
1024                     // it doesn't have to be explicitly imported so this information
1025                     // is useless. Instead just see if it was parsed out.
1026 
1027                     PackageType pt = packageManager.getPackageType(fqpnPackage);
1028 
1029                     if (pt != null) {
1030                         ClassType ct = pt.getClassType(fqpnClass);
1031 
1032                         if (ct != null) {
1033                             // OK. the user specified a full package to be imported
1034                             // that is in the package manager so it is time to
1035                             // link to it.
1036 
1037                             line = xrLine(line, pt.getName(), ct);
1038                         }
1039                     }
1040 
1041                     if (fqpnPackage.equals(currentImport.getName()) && currentImport.getClassType(fqpnClass) != null) {
1042                         // then the package we are currently in is the one specified in the string
1043                         // and the import class is correct.
1044                         line = xrLine(line, pkg, currentImport.getClassType(fqpnClass));
1045                     }
1046                 } else if (currentImport.getClassType(wordName) != null) {
1047                     line = xrLine(line, pkg, currentImport.getClassType(wordName));
1048                 }
1049             }
1050         }
1051 
1052         return importFilter(line);
1053     }
1054 
1055     /**
1056      * Given the current package, get an HREF to the package and class given
1057      *
1058      * @param dest destination
1059      * @param jc class type
1060      * @return href
1061      */
1062     private String getHREF(String dest, ClassType jc) {
1063         StringBuilder href = new StringBuilder();
1064 
1065         // find out how to go back to the root
1066         href.append(this.getPackageRoot());
1067 
1068         // now find out how to get to the dest package
1069         dest = dest.replace(".*", "").replace('.', '/');
1070 
1071         href.append(dest);
1072 
1073         // Now append filename.html
1074         if (jc != null) {
1075             href.append('/');
1076             href.append(jc.getFilename());
1077             href.append(".html");
1078             href.append('#');
1079             href.append(jc.getName());
1080         }
1081 
1082         return href.toString();
1083     }
1084 
1085     /**
1086      * Based on the destination package, get the HREF.
1087      *
1088      * @param dest destination
1089      * @return href
1090      */
1091     private String getHREF(String dest) {
1092         return getHREF(dest, null);
1093     }
1094 
1095     /**
1096      * <p>
1097      * Given the name of a package... get the number of subdirectories/subpackages there would be.
1098      * </p>
1099      * <p>
1100      * EX: {@code org.apache.maven == 3}
1101      * </p>
1102      *
1103      * @param packageName String
1104      * @return int
1105      */
1106     private int getPackageCount(String packageName) {
1107         if (packageName == null) {
1108             return 0;
1109         }
1110 
1111         int count = 0;
1112         int index = 0;
1113 
1114         while (true) {
1115             index = packageName.indexOf('.', index);
1116 
1117             if (index == -1) {
1118                 break;
1119             }
1120             ++index;
1121             ++count;
1122         }
1123 
1124         // need to increment this by one
1125         ++count;
1126 
1127         return count;
1128     }
1129 
1130     /**
1131      * Parse out the current link and look for package/import statements and then create HREFs for them
1132      *
1133      * @param line line
1134      * @return processed line
1135      */
1136     private String importFilter(String line) {
1137         int start = -1;
1138 
1139         /*
1140          * Used for determining if this is a package declaration. If it is then we can make some additional assumptions:
1141          * - that this isn't a Class import so the full String is valid - that it WILL be on the disk since this is
1142          * based on the current - file.
1143          */
1144         boolean isPackage = line.trim().startsWith("package ");
1145         boolean isImport = line.trim().startsWith("import ");
1146 
1147         if (isImport || isPackage) {
1148             start = line.trim().indexOf(' ');
1149         }
1150 
1151         if (start != -1) {
1152             // filter out this packagename...
1153             String pkg = line.substring(start).trim();
1154 
1155             // specify the classname of this import if any.
1156             String classname = null;
1157 
1158             if (pkg.contains(".*")) {
1159                 pkg = pkg.replace(".*", "");
1160             } else if (!isPackage) {
1161                 // this is an explicit Class import
1162 
1163                 String packageLine = pkg;
1164 
1165                 // This catches a boundary problem where you have something like:
1166                 //
1167                 // Foo foo = FooMaster.getFooInstance().
1168                 // danceLittleFoo();
1169                 //
1170                 // This breaks Jxr and won't be a problem when we hook
1171                 // in the real parser.
1172 
1173                 int a = packageLine.lastIndexOf('.') + 1;
1174                 int b = packageLine.length() - 1;
1175 
1176                 if (a > b + 1) {
1177                     classname = packageLine.substring(packageLine.lastIndexOf('.') + 1, packageLine.length() - 1);
1178 
1179                     int end = pkg.lastIndexOf('.');
1180                     if (end == -1) {
1181                         end = pkg.length() - 1;
1182                     }
1183 
1184                     pkg = pkg.substring(0, end);
1185                 }
1186             }
1187 
1188             pkg = pkg.replace(";", "");
1189             String pkgHREF = getHREF(pkg);
1190             // if this package is within the PackageManager then you can create an HREF for it.
1191 
1192             if (packageManager.getPackageType(pkg) != null || isPackage) {
1193                 // Create an HREF for explicit classname imports
1194                 if (classname != null) {
1195                     line = line.replace(
1196                             classname, "<a href=\"" + pkgHREF + '/' + classname + ".html" + "\">" + classname + "</a>");
1197                 }
1198 
1199                 // now replace the given package with a href
1200                 line = line.replace(pkg, "<a href=\"" + pkgHREF + '/' + DirectoryIndexer.INDEX + "\">" + pkg + "</a>");
1201             }
1202         }
1203 
1204         return line;
1205     }
1206 
1207     /**
1208      * if the given char is not one of the following in VALID_URI_CHARS then return true
1209      *
1210      * @param c char to check against VALID_URI_CHARS list
1211      * @return {@code true} if c is a valid URI char
1212      */
1213     private boolean isInvalidURICharacter(char c) {
1214         for (char validUriChar : VALID_URI_CHARS) {
1215             if (validUriChar == c) {
1216                 return false;
1217             }
1218         }
1219 
1220         return true;
1221     }
1222 }