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(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      * Analyze a JAR and find any classes and their details. Note that if the provided JAR analyzer has previously
105      * analyzed the JAR, the cached results will be returned. You must obtain a new JAR analyzer to the re-read the
106      * contents of the file.
107      *
108      * @param jarAnalyzer the JAR to analyze. This must not yet have been closed.
109      * @return the details of the classes found
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         // ordered by increasing Java version
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                     // ignore the module-info.class for computing the maxVersion, since it will always be >= 9
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             // the one and only class file was module-info.class
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 }