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(69.0, "25");
76 aMap.put(68.0, "24");
77 aMap.put(67.0, "23");
78 aMap.put(66.0, "22");
79 aMap.put(65.0, "21");
80 aMap.put(64.0, "20");
81 aMap.put(63.0, "19");
82 aMap.put(62.0, "18");
83 aMap.put(61.0, "17");
84 aMap.put(60.0, "16");
85 aMap.put(59.0, "15");
86 aMap.put(58.0, "14");
87 aMap.put(57.0, "13");
88 aMap.put(56.0, "12");
89 aMap.put(55.0, "11");
90 aMap.put(54.0, "10");
91 aMap.put(53.0, "9");
92 aMap.put(52.0, "1.8");
93 aMap.put(51.0, "1.7");
94 aMap.put(50.0, "1.6");
95 aMap.put(49.0, "1.5");
96 aMap.put(48.0, "1.4");
97 aMap.put(47.0, "1.3");
98 aMap.put(46.0, "1.2");
99 aMap.put(45.3, "1.1");
100 JAVA_CLASS_VERSIONS = Collections.unmodifiableMap(aMap);
101 }
102
103
104
105
106
107
108
109
110
111 public JarClasses analyze(JarAnalyzer jarAnalyzer) {
112 JarData jarData = jarAnalyzer.getJarData();
113 JarClasses classes = jarData.getJarClasses();
114 if (classes == null) {
115 if (jarData.isMultiRelease()) {
116 classes = analyzeMultiRelease(jarAnalyzer);
117 } else {
118 classes = analyzeRoot(jarAnalyzer);
119 }
120 }
121 return classes;
122 }
123
124 private Integer jarEntryVersion(JarEntry entry) {
125 Matcher matcher = ENTRY_FILTER_MULTI_RELEASE.matcher(entry.getName());
126 if (matcher.matches()) {
127 return Integer.valueOf(matcher.group(1));
128 }
129 return ROOT;
130 }
131
132 private JarClasses analyzeMultiRelease(JarAnalyzer jarAnalyzer) {
133 String jarFilename = jarAnalyzer.getFile().getAbsolutePath();
134
135 Map<Integer, List<JarEntry>> mapEntries =
136 jarAnalyzer.getEntries().stream().collect(Collectors.groupingBy(this::jarEntryVersion));
137
138
139 NavigableMap<Integer, JarVersionedRuntime> runtimeVersionsMap = new TreeMap<>();
140
141 for (Map.Entry<Integer, List<JarEntry>> mapEntry : mapEntries.entrySet()) {
142 Integer runtimeVersion = mapEntry.getKey();
143 List<JarEntry> runtimeVersionEntryList = mapEntry.getValue();
144
145 List<JarEntry> classList = jarAnalyzer.getClassEntries(runtimeVersionEntryList);
146
147 JarClasses classes = analyze(jarFilename, classList);
148
149 runtimeVersionsMap.put(runtimeVersion, new JarVersionedRuntime(runtimeVersionEntryList, classes));
150 }
151
152 JarData jarData = jarAnalyzer.getJarData();
153
154 JarVersionedRuntime rootContentVersionedRuntime = runtimeVersionsMap.remove(ROOT);
155 jarData.setRootEntries(rootContentVersionedRuntime.getEntries());
156 JarClasses rootJarClasses = rootContentVersionedRuntime.getJarClasses();
157 jarData.setJarClasses(rootJarClasses);
158
159 jarData.setVersionedRuntimes(new JarVersionedRuntimes(runtimeVersionsMap));
160
161 return rootJarClasses;
162 }
163
164 private JarClasses analyzeRoot(JarAnalyzer jarAnalyzer) {
165 String jarFilename = jarAnalyzer.getFile().getAbsolutePath();
166
167 List<JarEntry> classList = jarAnalyzer.getClassEntries();
168
169 JarClasses classes = analyze(jarFilename, classList);
170
171 jarAnalyzer.getJarData().setJarClasses(classes);
172 return classes;
173 }
174
175 private JarClasses analyze(String jarFilename, List<JarEntry> classList) {
176 JarClasses classes = new JarClasses();
177
178 classes.setDebugPresent(false);
179
180 double maxVersion = 0.0;
181 double moduleInfoVersion = 0.0;
182
183 for (JarEntry entry : classList) {
184 String classname = entry.getName();
185
186 try {
187 ClassParser classParser = new ClassParser(jarFilename, classname);
188
189 JavaClass javaClass = classParser.parse();
190
191 String classSignature = javaClass.getClassName();
192
193 if (!classes.isDebugPresent()) {
194 if (hasDebugSymbols(javaClass)) {
195 classes.setDebugPresent(true);
196 }
197 }
198
199 double classVersion = javaClass.getMajor();
200 if (javaClass.getMinor() > 0) {
201 classVersion = classVersion + javaClass.getMinor() / 10.0;
202 }
203
204 if ("module-info".equals(classSignature)) {
205
206 moduleInfoVersion = classVersion;
207 } else if (classVersion > maxVersion) {
208 maxVersion = classVersion;
209 }
210
211 Method[] methods = javaClass.getMethods();
212 for (Method method : methods) {
213 classes.addMethod(classSignature + "." + method.getName() + method.getSignature());
214 }
215
216 String classPackageName = javaClass.getPackageName();
217
218 classes.addClassName(classSignature);
219 classes.addPackage(classPackageName);
220
221 ImportVisitor importVisitor = new ImportVisitor(javaClass);
222 DescendingVisitor descVisitor = new DescendingVisitor(javaClass, importVisitor);
223 javaClass.accept(descVisitor);
224
225 classes.addImports(importVisitor.getImports());
226 } catch (ClassFormatException e) {
227 logger.warn("Unable to process class " + classname + " in JarAnalyzer File " + jarFilename, e);
228 } catch (IOException e) {
229 logger.warn("Unable to process JarAnalyzer File " + jarFilename, e);
230 }
231 }
232
233 if (maxVersion == 0.0 && moduleInfoVersion > 0.0) {
234
235 maxVersion = moduleInfoVersion;
236 }
237
238 Optional.ofNullable(JAVA_CLASS_VERSIONS.get(maxVersion)).ifPresent(classes::setJdkRevision);
239
240 return classes;
241 }
242
243 private boolean hasDebugSymbols(JavaClass javaClass) {
244 boolean ret = false;
245 Method[] methods = javaClass.getMethods();
246 for (Method method : methods) {
247 LineNumberTable linenumbers = method.getLineNumberTable();
248 if (linenumbers != null && linenumbers.getLength() > 0) {
249 ret = true;
250 break;
251 }
252 }
253 return ret;
254 }
255 }