1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.shared.jar.classes;
20
21 import javax.inject.Named;
22 import javax.inject.Singleton;
23
24 import java.io.IOException;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.NavigableMap;
30 import java.util.Optional;
31 import java.util.TreeMap;
32 import java.util.jar.JarEntry;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import java.util.stream.Collectors;
36
37 import org.apache.bcel.classfile.ClassFormatException;
38 import org.apache.bcel.classfile.ClassParser;
39 import org.apache.bcel.classfile.DescendingVisitor;
40 import org.apache.bcel.classfile.JavaClass;
41 import org.apache.bcel.classfile.LineNumberTable;
42 import org.apache.bcel.classfile.Method;
43 import org.apache.maven.shared.jar.JarAnalyzer;
44 import org.apache.maven.shared.jar.JarData;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48
49
50
51
52
53
54
55
56 @Singleton
57 @Named
58 @SuppressWarnings("checkstyle:MagicNumber")
59 public class JarClassesAnalysis {
60
61 private final Logger logger = LoggerFactory.getLogger(getClass());
62
63
64
65
66
67 private static final Integer ROOT = 0;
68
69 private static final Pattern ENTRY_FILTER_MULTI_RELEASE = Pattern.compile("^META-INF/versions/([1-9]\\d*)/.*$");
70
71 private static final Map<Double, String> JAVA_CLASS_VERSIONS;
72
73 static {
74 HashMap<Double, String> aMap = new HashMap<>();
75 aMap.put(65.0, "21");
76 aMap.put(64.0, "20");
77 aMap.put(63.0, "19");
78 aMap.put(62.0, "18");
79 aMap.put(61.0, "17");
80 aMap.put(60.0, "16");
81 aMap.put(59.0, "15");
82 aMap.put(58.0, "14");
83 aMap.put(57.0, "13");
84 aMap.put(56.0, "12");
85 aMap.put(55.0, "11");
86 aMap.put(54.0, "10");
87 aMap.put(53.0, "9");
88 aMap.put(52.0, "1.8");
89 aMap.put(51.0, "1.7");
90 aMap.put(50.0, "1.6");
91 aMap.put(49.0, "1.5");
92 aMap.put(48.0, "1.4");
93 aMap.put(47.0, "1.3");
94 aMap.put(46.0, "1.2");
95 aMap.put(45.3, "1.1");
96 JAVA_CLASS_VERSIONS = Collections.unmodifiableMap(aMap);
97 }
98
99
100
101
102
103
104
105
106
107 public JarClasses analyze(JarAnalyzer jarAnalyzer) {
108 JarData jarData = jarAnalyzer.getJarData();
109 JarClasses classes = jarData.getJarClasses();
110 if (classes == null) {
111 if (jarData.isMultiRelease()) {
112 classes = analyzeMultiRelease(jarAnalyzer);
113 } else {
114 classes = analyzeRoot(jarAnalyzer);
115 }
116 }
117 return classes;
118 }
119
120 private Integer jarEntryVersion(JarEntry entry) {
121 Matcher matcher = ENTRY_FILTER_MULTI_RELEASE.matcher(entry.getName());
122 if (matcher.matches()) {
123 return Integer.valueOf(matcher.group(1));
124 }
125 return ROOT;
126 }
127
128 private JarClasses analyzeMultiRelease(JarAnalyzer jarAnalyzer) {
129 String jarFilename = jarAnalyzer.getFile().getAbsolutePath();
130
131 Map<Integer, List<JarEntry>> mapEntries =
132 jarAnalyzer.getEntries().stream().collect(Collectors.groupingBy(this::jarEntryVersion));
133
134
135 NavigableMap<Integer, JarVersionedRuntime> runtimeVersionsMap = new TreeMap<>();
136
137 for (Map.Entry<Integer, List<JarEntry>> mapEntry : mapEntries.entrySet()) {
138 Integer runtimeVersion = mapEntry.getKey();
139 List<JarEntry> runtimeVersionEntryList = mapEntry.getValue();
140
141 List<JarEntry> classList = jarAnalyzer.getClassEntries(runtimeVersionEntryList);
142
143 JarClasses classes = analyze(jarFilename, classList);
144
145 runtimeVersionsMap.put(runtimeVersion, new JarVersionedRuntime(runtimeVersionEntryList, classes));
146 }
147
148 JarData jarData = jarAnalyzer.getJarData();
149
150 JarVersionedRuntime rootContentVersionedRuntime = runtimeVersionsMap.remove(ROOT);
151 jarData.setRootEntries(rootContentVersionedRuntime.getEntries());
152 JarClasses rootJarClasses = rootContentVersionedRuntime.getJarClasses();
153 jarData.setJarClasses(rootJarClasses);
154
155 jarData.setVersionedRuntimes(new JarVersionedRuntimes(runtimeVersionsMap));
156
157 return rootJarClasses;
158 }
159
160 private JarClasses analyzeRoot(JarAnalyzer jarAnalyzer) {
161 String jarFilename = jarAnalyzer.getFile().getAbsolutePath();
162
163 List<JarEntry> classList = jarAnalyzer.getClassEntries();
164
165 JarClasses classes = analyze(jarFilename, classList);
166
167 jarAnalyzer.getJarData().setJarClasses(classes);
168 return classes;
169 }
170
171 private JarClasses analyze(String jarFilename, List<JarEntry> classList) {
172 JarClasses classes = new JarClasses();
173
174 classes.setDebugPresent(false);
175
176 double maxVersion = 0.0;
177 double moduleInfoVersion = 0.0;
178
179 for (JarEntry entry : classList) {
180 String classname = entry.getName();
181
182 try {
183 ClassParser classParser = new ClassParser(jarFilename, classname);
184
185 JavaClass javaClass = classParser.parse();
186
187 String classSignature = javaClass.getClassName();
188
189 if (!classes.isDebugPresent()) {
190 if (hasDebugSymbols(javaClass)) {
191 classes.setDebugPresent(true);
192 }
193 }
194
195 double classVersion = javaClass.getMajor();
196 if (javaClass.getMinor() > 0) {
197 classVersion = classVersion + javaClass.getMinor() / 10.0;
198 }
199
200 if ("module-info".equals(classSignature)) {
201
202 moduleInfoVersion = classVersion;
203 } else if (classVersion > maxVersion) {
204 maxVersion = classVersion;
205 }
206
207 Method[] methods = javaClass.getMethods();
208 for (Method method : methods) {
209 classes.addMethod(classSignature + "." + method.getName() + method.getSignature());
210 }
211
212 String classPackageName = javaClass.getPackageName();
213
214 classes.addClassName(classSignature);
215 classes.addPackage(classPackageName);
216
217 ImportVisitor importVisitor = new ImportVisitor(javaClass);
218 DescendingVisitor descVisitor = new DescendingVisitor(javaClass, importVisitor);
219 javaClass.accept(descVisitor);
220
221 classes.addImports(importVisitor.getImports());
222 } catch (ClassFormatException e) {
223 logger.warn("Unable to process class " + classname + " in JarAnalyzer File " + jarFilename, e);
224 } catch (IOException e) {
225 logger.warn("Unable to process JarAnalyzer File " + jarFilename, e);
226 }
227 }
228
229 if (maxVersion == 0.0 && moduleInfoVersion > 0.0) {
230
231 maxVersion = moduleInfoVersion;
232 }
233
234 Optional.ofNullable(JAVA_CLASS_VERSIONS.get(maxVersion)).ifPresent(classes::setJdkRevision);
235
236 return classes;
237 }
238
239 private boolean hasDebugSymbols(JavaClass javaClass) {
240 boolean ret = false;
241 Method[] methods = javaClass.getMethods();
242 for (Method method : methods) {
243 LineNumberTable linenumbers = method.getLineNumberTable();
244 if (linenumbers != null && linenumbers.getLength() > 0) {
245 ret = true;
246 break;
247 }
248 }
249 return ret;
250 }
251 }