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.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   * Analyze the classes in a JAR file. This class is thread safe and immutable as it retains no state.
50   *
51   * Note that you must first create an instance of {@link org.apache.maven.shared.jar.JarAnalyzer} - see its Javadoc for
52   * a typical use.
53   *
54   * @see #analyze(org.apache.maven.shared.jar.JarAnalyzer)
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       * Constant representing the classes in the root of a Multi-Release JAR file.
65       * Meaning outside of any given META-INF/versions/NN/... entry.
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      * Analyze a JAR and find any classes and their details. Note that if the provided JAR analyzer has previously
101      * analyzed the JAR, the cached results will be returned. You must obtain a new JAR analyzer to the re-read the
102      * contents of the file.
103      *
104      * @param jarAnalyzer the JAR to analyze. This must not yet have been closed.
105      * @return the details of the classes found
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         // ordered by increasing Java version
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         // Paranoid?
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                     // ignore the module-info.class for computing the maxVersion, since it will always be >= 9
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             // the one and only class file was module-info.class
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 }