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/(\\d{1,2})/.+$");
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 JarVersionedRuntime baseJarRelease = runtimeVersionsMap.remove(ROOT);
149 JarClasses baseJarClasses = baseJarRelease.getJarClasses();
150
151 jarAnalyzer.getJarData().setJarClasses(baseJarClasses);
152
153
154 for (Map.Entry<Integer, JarVersionedRuntime> runtimeVersionEntry : runtimeVersionsMap.entrySet()) {
155 Integer version = runtimeVersionEntry.getKey();
156 String jdkRevision = runtimeVersionEntry.getValue().getJarClasses().getJdkRevision();
157 if (!version.equals(Integer.valueOf(jdkRevision))) {
158 logger.warn(
159 "Multi-release version {} in JAR file '{}' has some class compiled for Jdk revision {}",
160 version,
161 jarFilename,
162 jdkRevision);
163 }
164 }
165
166 jarAnalyzer.getJarData().setVersionedRuntimes(new JarVersionedRuntimes(runtimeVersionsMap));
167
168 return baseJarClasses;
169 }
170
171 private JarClasses analyzeRoot(JarAnalyzer jarAnalyzer) {
172 String jarFilename = jarAnalyzer.getFile().getAbsolutePath();
173
174 List<JarEntry> classList = jarAnalyzer.getClassEntries();
175
176 JarClasses classes = analyze(jarFilename, classList);
177
178 jarAnalyzer.getJarData().setJarClasses(classes);
179 return classes;
180 }
181
182 private JarClasses analyze(String jarFilename, List<JarEntry> classList) {
183 JarClasses classes = new JarClasses();
184
185 classes.setDebugPresent(false);
186
187 double maxVersion = 0.0;
188 double moduleInfoVersion = 0.0;
189
190 for (JarEntry entry : classList) {
191 String classname = entry.getName();
192
193 try {
194 ClassParser classParser = new ClassParser(jarFilename, classname);
195
196 JavaClass javaClass = classParser.parse();
197
198 String classSignature = javaClass.getClassName();
199
200 if (!classes.isDebugPresent()) {
201 if (hasDebugSymbols(javaClass)) {
202 classes.setDebugPresent(true);
203 }
204 }
205
206 double classVersion = javaClass.getMajor();
207 if (javaClass.getMinor() > 0) {
208 classVersion = classVersion + javaClass.getMinor() / 10.0;
209 }
210
211 if ("module-info".equals(classSignature)) {
212
213 moduleInfoVersion = classVersion;
214 } else if (classVersion > maxVersion) {
215 maxVersion = classVersion;
216 }
217
218 Method[] methods = javaClass.getMethods();
219 for (Method method : methods) {
220 classes.addMethod(classSignature + "." + method.getName() + method.getSignature());
221 }
222
223 String classPackageName = javaClass.getPackageName();
224
225 classes.addClassName(classSignature);
226 classes.addPackage(classPackageName);
227
228 ImportVisitor importVisitor = new ImportVisitor(javaClass);
229 DescendingVisitor descVisitor = new DescendingVisitor(javaClass, importVisitor);
230 javaClass.accept(descVisitor);
231
232 classes.addImports(importVisitor.getImports());
233 } catch (ClassFormatException e) {
234 logger.warn("Unable to process class " + classname + " in JarAnalyzer File " + jarFilename, e);
235 } catch (IOException e) {
236 logger.warn("Unable to process JarAnalyzer File " + jarFilename, e);
237 }
238 }
239
240 if (maxVersion == 0.0 && moduleInfoVersion > 0.0) {
241
242 maxVersion = moduleInfoVersion;
243 }
244
245 Optional.ofNullable(JAVA_CLASS_VERSIONS.get(maxVersion)).ifPresent(classes::setJdkRevision);
246
247 return classes;
248 }
249
250 private boolean hasDebugSymbols(JavaClass javaClass) {
251 boolean ret = false;
252 Method[] methods = javaClass.getMethods();
253 for (Method method : methods) {
254 LineNumberTable linenumbers = method.getLineNumberTable();
255 if (linenumbers != null && linenumbers.getLength() > 0) {
256 ret = true;
257 break;
258 }
259 }
260 return ret;
261 }
262 }