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 }