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.pacman;
20  
21  import java.io.FileInputStream;
22  import java.io.FileReader;
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.io.Reader;
26  import java.io.StreamTokenizer;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.util.Arrays;
30  import java.util.List;
31  
32  /**
33   * PacMan implementation of a JavaFile. This will parse out the file and
34   * determine package, class, and imports
35   *
36   * @author <a href="mailto:burton@apache.org">Kevin A. Burton</a>
37   */
38  public class JavaFileImpl extends JavaFile {
39  
40      private final List<String> classTypes = Arrays.asList("class", "interface", "enum", "record");
41  
42      /**
43       * Constructor of a new object that points to a given file.
44       *
45       * @param path path of the file
46       * @param encoding encoding of the file
47       * @throws IOException on parsing failure
48       */
49      public JavaFileImpl(Path path, String encoding) throws IOException {
50          super(path, encoding);
51  
52          // always add java.lang.* to the package imports because the JVM always
53          // does this implicitly.  Unless we add this to the ImportTypes JXR
54          // won't pick up on this.
55          this.addImportType(new ImportType("java.lang.*"));
56  
57          // now parse out this file.
58          this.parse();
59      }
60  
61      /**
62       * Opens up the file and try to determine package, class and import statements.
63       */
64      private void parse() throws IOException {
65          StreamTokenizer stok = null;
66          try (Reader reader = getReader()) {
67              stok = this.getTokenizer(reader);
68  
69              parseRecursive("", stok);
70          } finally {
71              stok = null;
72          }
73      }
74  
75      private void parseRecursive(String nestedPrefix, StreamTokenizer stok) throws IOException {
76          int openBracesCount = 0;
77  
78          char prevttype = Character.MIN_VALUE; // previous token type
79          boolean inTripleQuote = false; // used to toggle between inside/outside triple-quoted multi-line strings
80  
81          while (stok.nextToken() != StreamTokenizer.TT_EOF) {
82  
83              if (stok.sval == null) {
84                  if (stok.ttype == '{') {
85                      openBracesCount++;
86                  } else if (stok.ttype == '}') {
87                      if (--openBracesCount == 0) {
88                          // break out of recursive
89                          return;
90                      }
91                  }
92                  continue;
93              } else {
94                  if ('"' == stok.ttype && '"' == prevttype) {
95                      inTripleQuote = !inTripleQuote;
96                  }
97                  prevttype = (char) stok.ttype;
98                  if (inTripleQuote) {
99                      // skip content found inside triple-quoted multi-line Java 15 String
100                     continue;
101                 }
102             }
103 
104             // set the package
105             if ("package".equals(stok.sval) && stok.ttype != '\"') {
106                 stok.nextToken();
107                 if (stok.sval != null) {
108                     this.setPackageType(new PackageType(stok.sval));
109                 }
110             }
111 
112             // set the imports
113             if ("import".equals(stok.sval) && stok.ttype != '\"') {
114                 stok.nextToken();
115 
116                 String name = stok.sval;
117 
118                 /*
119                 WARNING: this is a bug/non-feature in the current
120                 StreamTokenizer.  We needed to set the comment char as "*"
121                 and packages that are imported with this (ex "test.*") will be
122                 stripped( and become "test." ).  Here we need to test for this
123                 and if necessary re-add the char.
124                 */
125                 if (name != null) {
126                     if (name.charAt(name.length() - 1) == '.') {
127                         name = name + '*';
128                     }
129                     this.addImportType(new ImportType(name));
130                 }
131             }
132 
133             // Add the class or classes. There can be several classes in one file so
134             // continue with the while loop to get them all.
135             if (classTypes.contains(stok.sval) && stok.ttype != '"') {
136                 stok.nextToken();
137                 if (stok.sval != null) {
138                     this.addClassType(
139                             new ClassType(nestedPrefix + stok.sval, getFilenameWithoutPathOrExtension(this.getPath())));
140                     parseRecursive(nestedPrefix + stok.sval + ".", stok);
141                 }
142             }
143         }
144     }
145 
146     /**
147      * Gets a {@link StreamTokenizer} for this file.
148      */
149     private StreamTokenizer getTokenizer(Reader reader) {
150         StreamTokenizer stok = new StreamTokenizer(reader);
151 
152         stok.commentChar('*');
153         stok.wordChars('_', '_');
154 
155         // set tokenizer to skip comments
156         stok.slashStarComments(true);
157         stok.slashSlashComments(true);
158 
159         return stok;
160     }
161 
162     private Reader getReader() throws IOException {
163         if (Files.notExists(this.getPath())) {
164             throw new IOException(this.getPath() + " does not exist!");
165         }
166 
167         if (this.getEncoding() != null) {
168             return new InputStreamReader(new FileInputStream(this.getPath().toFile()), this.getEncoding());
169         } else {
170             return new FileReader(this.getPath().toFile());
171         }
172     }
173 }