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 root content of a Multi-Release JAR file, thus outside of
65       * any given META-INF/versions/N/... entry.
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      * 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         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                     // ignore the module-info.class for computing the maxVersion, since it will always be >= 9
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             // the one and only class file was module-info.class
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 }