1
18 package net.bull.javamelody.internal.model;
19
20 import java.io.BufferedInputStream;
21 import java.io.ByteArrayInputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.io.Serializable;
29 import java.net.URL;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.LinkedHashMap;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Properties;
38 import java.util.Set;
39 import java.util.TreeMap;
40 import java.util.zip.ZipEntry;
41 import java.util.zip.ZipInputStream;
42
43 import javax.servlet.ServletContext;
44 import javax.xml.parsers.DocumentBuilder;
45 import javax.xml.parsers.DocumentBuilderFactory;
46 import javax.xml.parsers.ParserConfigurationException;
47
48 import org.w3c.dom.Document;
49 import org.w3c.dom.Node;
50 import org.w3c.dom.NodeList;
51 import org.xml.sax.SAXException;
52
53 import net.bull.javamelody.Parameter;
54 import net.bull.javamelody.internal.common.InputOutput;
55 import net.bull.javamelody.internal.common.LOG;
56 import net.bull.javamelody.internal.common.Parameters;
57
58
62 public final class MavenArtifact implements Serializable {
63 private static final long serialVersionUID = 1L;
64
65 private static final String MAVEN_CENTRAL = "https:;
66
67 private static final File LOCAL_REPO = new File(
68 System.getProperty("user.home") + "/.m2/repository");
69
70 private static final String TOMCAT_ARCHIVES = "https:;
71
72 private static Map<String, String> sourceFilePathsByJarFileNames;
73
74 private static String webappVersion;
75
76 private String name;
77 private String url;
78 private String groupId;
79 private String artifactId;
80 private String version;
81 private MavenArtifact parent;
82 private final Map<String, String> licenseUrlsByName = new LinkedHashMap<>();
83 private Map<String, String> properties;
84 private final List<MavenArtifact> dependencies = new ArrayList<>();
85 private final List<MavenArtifact> managedDependencies = new ArrayList<>();
86 private boolean updated;
87
88 private MavenArtifact() {
89 super();
90 }
91
92 private static MavenArtifact parseDependency(URL jarFileLocation) throws IOException {
93 final byte[] pomXml = readMavenFileFromJarFile(jarFileLocation, "pom.xml");
94 if (pomXml != null) {
95 final MavenArtifact dependency = new MavenArtifact();
96 dependency.parsePomXml(new ByteArrayInputStream(pomXml));
97 return dependency;
98 }
99 return null;
100 }
101
102 private void parsePomXml(InputStream pomXml) throws IOException {
103 final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
104 try {
105 final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
106 final Document doc = dBuilder.parse(pomXml);
107 final Node projectNode = doc.getElementsByTagName("project").item(0);
108 final NodeList childNodes = projectNode.getChildNodes();
109 properties = new HashMap<>();
110 for (int i = 0; i < childNodes.getLength(); i++) {
111 final Node node = childNodes.item(i);
112 parseNode(node);
113 }
114 properties.put("project.groupId", groupId);
115 properties.put("pom.groupId", groupId);
116 properties.put("groupId", groupId);
117 properties.put("project.version", version);
118 properties.put("pom.version", version);
119 properties.put("version", version);
120 for (final MavenArtifact dependency : dependencies) {
121 dependency.groupId = replaceProperty(dependency.groupId, properties);
122 dependency.version = replaceProperty(dependency.version, properties);
123 }
124 for (final MavenArtifact dependency : managedDependencies) {
125 dependency.groupId = replaceProperty(dependency.groupId, properties);
126 dependency.version = replaceProperty(dependency.version, properties);
127 }
128 properties = null;
129 } catch (final ParserConfigurationException | SAXException e) {
130 throw new IOException(e.getMessage(), e);
131 }
132 updated = true;
133 }
134
135
136 private void parseNode(Node node) {
137
138 final String nodeName = node.getNodeName();
139 if ("name".equals(nodeName)) {
140 this.name = node.getTextContent();
141 } else if ("description".equals(nodeName) && this.name == null) {
142 this.name = node.getTextContent();
143 } else if ("url".equals(nodeName)) {
144 this.url = node.getTextContent();
145 } else if ("groupId".equals(nodeName) && !node.getTextContent().startsWith("${")) {
146 this.groupId = node.getTextContent();
147 } else if ("artifactId".equals(nodeName)) {
148 this.artifactId = node.getTextContent();
149 } else if ("version".equals(nodeName) && !node.getTextContent().startsWith("${")) {
150 this.version = node.getTextContent();
151 } else if ("parent".equals(nodeName)) {
152 parseParentNode(node);
153 } else if ("properties".equals(nodeName)) {
154 properties.putAll(parsePropertiesNode(node));
155 } else if ("licenses".equals(nodeName)) {
156 parseLicensesNode(node);
157 } else if ("dependencies".equals(nodeName)) {
158 this.dependencies.addAll(parseDependenciesNode(node));
159 } else if ("dependencyManagement".equals(nodeName)) {
160 final NodeList childNodes = node.getChildNodes();
161 for (int i = 0; i < childNodes.getLength(); i++) {
162 final Node childNode = childNodes.item(i);
163 if ("dependencies".equals(childNode.getNodeName())) {
164 this.managedDependencies.addAll(parseDependenciesNode(childNode));
165 }
166 }
167 }
168 }
169
170 private void parseParentNode(Node node) {
171 final NodeList parentNodes = node.getChildNodes();
172 String parentGroupId = null;
173 String parentArtifactId = null;
174 String parentVersion = null;
175 for (int k = 0; k < parentNodes.getLength(); k++) {
176 final Node childParentNode = parentNodes.item(k);
177 final String nodeName = childParentNode.getNodeName();
178 if ("groupId".equals(nodeName)) {
179 parentGroupId = childParentNode.getTextContent();
180 } else if ("artifactId".equals(nodeName)) {
181 parentArtifactId = childParentNode.getTextContent();
182 } else if ("version".equals(nodeName)) {
183 parentVersion = childParentNode.getTextContent();
184 }
185 }
186 if (this.groupId == null) {
187 this.groupId = parentGroupId;
188 }
189 if (this.version == null) {
190 this.version = parentVersion;
191 }
192 this.parent = new MavenArtifact();
193 this.parent.groupId = parentGroupId;
194 this.parent.artifactId = parentArtifactId;
195 this.parent.version = parentVersion;
196 }
197
198 private Map<String, String> parsePropertiesNode(Node propertiesNode) {
199 final Map<String, String> props = new HashMap<>();
200 final NodeList propertiesNodes = propertiesNode.getChildNodes();
201 for (int j = 0; j < propertiesNodes.getLength(); j++) {
202 final Node propertyNode = propertiesNodes.item(j);
203 final String nodeName = propertyNode.getNodeName();
204 if (nodeName != null) {
205 props.put(nodeName, propertyNode.getTextContent());
206 }
207 }
208 return props;
209 }
210
211 private void parseLicensesNode(Node licensesNode) {
212 final NodeList licenseNodes = licensesNode.getChildNodes();
213 for (int j = 0; j < licenseNodes.getLength(); j++) {
214 final Node licenseNode = licenseNodes.item(j);
215 if ("license".equals(licenseNode.getNodeName())) {
216 String licenseName = null;
217 String licenseUrl = null;
218 final NodeList childLicenseNodes = licenseNode.getChildNodes();
219 for (int k = 0; k < childLicenseNodes.getLength(); k++) {
220 final Node childLicenseNode = childLicenseNodes.item(k);
221 final String nodeName = childLicenseNode.getNodeName();
222 if ("name".equals(nodeName)) {
223 licenseName = childLicenseNode.getTextContent();
224 } else if ("url".equals(nodeName)) {
225 licenseUrl = childLicenseNode.getTextContent();
226 }
227 }
228 if (licenseName != null) {
229 licenseUrlsByName.put(licenseName, licenseUrl);
230 } else if (licenseUrl != null) {
231 if (licenseUrl.startsWith("http")) {
232 licenseUrlsByName.put("LICENSE", licenseUrl);
233 } else {
234 licenseUrlsByName.put(licenseUrl, licenseUrl);
235 }
236 }
237 }
238 }
239 }
240
241
242 private List<MavenArtifact> parseDependenciesNode(Node dependenciesNode) {
243
244 final List<MavenArtifact> deps = new ArrayList<>();
245 final NodeList dependencyNodes = dependenciesNode.getChildNodes();
246 for (int j = 0; j < dependencyNodes.getLength(); j++) {
247 final Node dependencyNode = dependencyNodes.item(j);
248 if ("dependency".equals(dependencyNode.getNodeName())) {
249 final NodeList childDependencyNodes = dependencyNode.getChildNodes();
250 final MavenArtifact dependency = new MavenArtifact();
251 String scope = null;
252 String optional = null;
253 for (int k = 0; k < childDependencyNodes.getLength(); k++) {
254 final Node childDependencyNode = childDependencyNodes.item(k);
255 final String nodeName = childDependencyNode.getNodeName();
256 if ("groupId".equals(nodeName)) {
257 dependency.groupId = childDependencyNode.getTextContent();
258 } else if ("artifactId".equals(nodeName)) {
259 dependency.artifactId = childDependencyNode.getTextContent();
260 } else if ("version".equals(nodeName)) {
261 dependency.version = childDependencyNode.getTextContent();
262 } else if ("scope".equals(nodeName)) {
263 scope = childDependencyNode.getTextContent();
264 } else if ("optional".equals(nodeName)) {
265 optional = childDependencyNode.getTextContent();
266 }
267 }
268 if ((scope == null || "compile".equals(scope)) && !"true".equals(optional)) {
269 deps.add(dependency);
270 }
271 }
272 }
273 return deps;
274 }
275
276 private void update() throws IOException {
277 if (!updated && version != null) {
278 updated = true;
279 final String filePath = getPath(".pom");
280 final File pomXml = getMavenArtifact(filePath);
281 if (pomXml != null) {
282 try (InputStream input = new FileInputStream(pomXml)) {
283 parsePomXml(input);
284 }
285 }
286 }
287 }
288
289 public String getGroupId() {
290 return groupId;
291 }
292
293 public String getArtifactId() {
294 return artifactId;
295 }
296
297 public String getVersion() {
298 return version;
299 }
300
301 public String getName() throws IOException {
302 if (name == null && parent != null) {
303 parent.update();
304 return parent.getName();
305 }
306 return name;
307 }
308
309 public String getUrl() throws IOException {
310 if (url == null && parent != null) {
311 parent.update();
312 return parent.getUrl();
313 }
314 return url;
315 }
316
317 public Map<String, String> getLicenseUrlsByName() throws IOException {
318 if (licenseUrlsByName.isEmpty() && parent != null) {
319 parent.update();
320 return parent.getLicenseUrlsByName();
321 }
322 return licenseUrlsByName;
323 }
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338 private List<MavenArtifact> getAllManagedDependencies() throws IOException {
339 update();
340 final List<MavenArtifact> allManagedDependencies = new ArrayList<>(managedDependencies);
341 if (parent != null) {
342 allManagedDependencies.addAll(parent.getAllManagedDependencies());
343 }
344 return allManagedDependencies;
345 }
346
347 List<MavenArtifact> getAllDependencies() throws IOException {
348 return getAllDependencies(1);
349 }
350
351 private List<MavenArtifact> getAllDependencies(int level) throws IOException {
352 if (level > 10) {
353
354 return Collections.emptyList();
355 }
356
357 update();
358 final List<MavenArtifact> transitiveDependencies = new ArrayList<>();
359 final List<MavenArtifact> allManagedDependencies = getAllManagedDependencies();
360 for (final MavenArtifact dependency : dependencies) {
361 if (dependency.version == null) {
362 for (final MavenArtifact managedDependency : allManagedDependencies) {
363 if (dependency.isSame(managedDependency)) {
364 dependency.version = managedDependency.version;
365 break;
366 }
367 }
368 }
369 transitiveDependencies.addAll(dependency.getAllDependencies(level + 1));
370 }
371 if (parent != null) {
372 transitiveDependencies.addAll(parent.getAllDependencies(level + 1));
373 }
374
375 final List<MavenArtifact> allDependencies = new ArrayList<>(dependencies);
376 for (final MavenArtifact transitiveDependency : transitiveDependencies) {
377 if (!transitiveDependency.isContained(allDependencies)) {
378 allDependencies.add(transitiveDependency);
379 }
380 }
381 return allDependencies;
382 }
383
384 private boolean isContained(List<MavenArtifact> artifacts) {
385 for (final MavenArtifact artifact : artifacts) {
386 if (isSame(artifact)) {
387 return true;
388 }
389 }
390 return false;
391 }
392
393 private boolean isSame(MavenArtifact artifact) {
394 return groupId.equals(artifact.groupId) && artifactId.equals(artifact.artifactId);
395 }
396
397 private String getPath(String extension) {
398 return groupId.replace('.', '/') + '/' + artifactId + '/' + version + '/' + artifactId + '-'
399 + version + extension;
400 }
401
402 public static Map<String, MavenArtifact> getWebappDependencies() throws IOException {
403
404
405 final Map<String, MavenArtifact> webappDependencies = getWebappDependenciesFromWebInfLib();
406
407
408
409
410 final List<MavenArtifact> allDependencies = new ArrayList<>(
411 getWebappDependenciesFromPomXml());
412 for (final MavenArtifact dependency : webappDependencies.values()) {
413 if (dependency != null && !dependency.isContained(allDependencies)) {
414 allDependencies.add(dependency);
415 for (final MavenArtifact transitiveDependency : dependency.getAllDependencies()) {
416 if (!transitiveDependency.isContained(allDependencies)) {
417 allDependencies.add(transitiveDependency);
418 }
419 }
420 }
421 }
422
423 for (final Map.Entry<String, MavenArtifact> entry : webappDependencies.entrySet()) {
424 if (entry.getValue() == null) {
425 final String jarFileName = entry.getKey();
426 for (final MavenArtifact dependency : allDependencies) {
427 if (jarFileName.startsWith(
428 dependency.getArtifactId() + '-' + dependency.getVersion())) {
429 entry.setValue(dependency);
430 break;
431 }
432 }
433 }
434 }
435 return webappDependencies;
436 }
437
438 private static List<MavenArtifact> getWebappDependenciesFromPomXml() throws IOException {
439 final InputStream webappPomXmlAsStream = getWebappPomXmlAsStream();
440 if (webappPomXmlAsStream != null) {
441 try {
442 final MavenArtifact webappArtifact = new MavenArtifact();
443 webappArtifact.parsePomXml(webappPomXmlAsStream);
444 return webappArtifact.getAllDependencies();
445 } finally {
446 webappPomXmlAsStream.close();
447 }
448 }
449 return Collections.emptyList();
450 }
451
452 private static Map<String, MavenArtifact> getWebappDependenciesFromWebInfLib()
453 throws IOException {
454 final ServletContext servletContext = Parameters.getServletContext();
455 final String directory = "/WEB-INF/lib/";
456
457 final Set<String> dependencies = servletContext.getResourcePaths(directory);
458
459
460
461 if (dependencies == null || dependencies.isEmpty()) {
462 return Collections.emptyMap();
463 }
464 final Map<String, MavenArtifact> result = new TreeMap<>();
465 for (final String dependency : dependencies) {
466 if (dependency.endsWith(".jar") || dependency.endsWith(".JAR")) {
467 final String fileName = dependency.substring(directory.length());
468 final URL jarFileLocation = servletContext.getResource(dependency);
469 if (jarFileLocation != null) {
470 result.put(fileName, parseDependency(jarFileLocation));
471 } else {
472 result.put(fileName, null);
473 }
474 }
475 }
476 return result;
477 }
478
479 public static InputStream getWebappPomXmlAsStream() {
480 return getWebappPomFile("pom.xml");
481 }
482
483 public static synchronized String getWebappVersion() {
484 if (webappVersion == null) {
485 webappVersion = Parameter.APPLICATION_VERSION.getValue();
486 if (webappVersion == null) {
487 final InputStream input = getWebappPomFile("pom.properties");
488 if (input != null) {
489 try {
490 try {
491 final Properties properties = new Properties();
492 properties.load(input);
493 webappVersion = properties.getProperty("version");
494 } finally {
495 input.close();
496 }
497 } catch (final IOException e) {
498 LOG.debug(e.toString(), e);
499 }
500 }
501 if (webappVersion == null) {
502
503 webappVersion = "";
504 }
505 }
506 }
507 if (webappVersion.isEmpty()) {
508 return null;
509 }
510 return webappVersion;
511 }
512
513 private static InputStream getWebappPomFile(String pomFilename) {
514 final Set<?> mavenDir = Parameters.getServletContext().getResourcePaths("/META-INF/maven/");
515 if (mavenDir == null || mavenDir.isEmpty()) {
516 return null;
517 }
518 final Set<?> groupDir = Parameters.getServletContext()
519 .getResourcePaths((String) mavenDir.iterator().next());
520 if (groupDir == null || groupDir.isEmpty()) {
521 return null;
522 }
523 final InputStream pomXml = Parameters.getServletContext()
524 .getResourceAsStream(groupDir.iterator().next() + pomFilename);
525 if (pomXml == null) {
526 return null;
527 }
528 return new BufferedInputStream(pomXml);
529 }
530
531 public static File getSourceJarFile(URL classesJarFileUrl) throws IOException {
532 final String file = classesJarFileUrl.getFile();
533 if (file.endsWith(".jar")) {
534 final File sources = new File(file.replace(".jar", "-sources.jar"));
535 if (sources.exists()) {
536 return sources;
537 }
538 }
539 final byte[] pomProperties = readMavenFileFromJarFile(classesJarFileUrl, "pom.properties");
540 if (pomProperties == null) {
541 final Map<String, String> sourceFilePaths = getSourceFilePathsByJarFileNames();
542 String jarFileName = file;
543 if (jarFileName.endsWith("!/")) {
544
545 jarFileName = jarFileName.substring(0, jarFileName.length() - "!/".length());
546 }
547 jarFileName = jarFileName.substring(jarFileName.lastIndexOf('/') + 1);
548 final String sourceFilePath = sourceFilePaths.get(jarFileName);
549 if (sourceFilePath != null) {
550 return getMavenArtifact(sourceFilePath);
551 }
552 return null;
553 }
554 final Properties properties = new Properties();
555 properties.load(new ByteArrayInputStream(pomProperties));
556 final MavenArtifact mavenArtifact = new MavenArtifact();
557 mavenArtifact.groupId = properties.getProperty("groupId");
558 mavenArtifact.artifactId = properties.getProperty("artifactId");
559 mavenArtifact.version = properties.getProperty("version");
560 final String filePath = mavenArtifact.getPath("-sources.jar");
561 return getMavenArtifact(filePath);
562 }
563
564 private static synchronized Map<String, String> getSourceFilePathsByJarFileNames()
565 throws IOException {
566 if (sourceFilePathsByJarFileNames == null) {
567 final Map<String, MavenArtifact> webappDependencies = getWebappDependencies();
568 final Map<String, String> sourceFilePaths = new HashMap<>();
569 for (final Map.Entry<String, MavenArtifact> entry : webappDependencies.entrySet()) {
570 final String jarFileName = entry.getKey();
571 final MavenArtifact dependency = entry.getValue();
572 if (dependency != null) {
573 final String filePath = dependency.getPath("-sources.jar");
574 sourceFilePaths.put(jarFileName, filePath);
575 }
576 }
577 sourceFilePathsByJarFileNames = sourceFilePaths;
578 }
579 return sourceFilePathsByJarFileNames;
580 }
581
582 private static File getMavenArtifact(String filePath) {
583 if (filePath.contains("${")) {
584
585
586 return null;
587 }
588 final File storageDirectory = Parameters
589 .getStorageDirectory(Parameters.getCurrentApplication());
590 final String subDirectory;
591 if (filePath.endsWith(".pom")) {
592 subDirectory = "poms";
593 } else {
594 subDirectory = "sources";
595 }
596 final File file = new File(storageDirectory,
597 subDirectory + '/' + filePath.substring(filePath.lastIndexOf('/') + 1));
598 if (!file.exists() || file.length() == 0) {
599 for (final String mavenRepository : getMavenRepositories()) {
600 final String url = mavenRepository + '/' + filePath;
601 if (!url.startsWith("http")) {
602 if (!new File(url).exists()) {
603 continue;
604 }
605 return new File(url);
606 }
607 mkdirs(file.getParentFile());
608 try (OutputStream output = new FileOutputStream(file)) {
609 final LabradorRetriever labradorRetriever = new LabradorRetriever(new URL(url));
610 labradorRetriever.downloadTo(output);
611
612 break;
613 } catch (final IOException e) {
614 InputOutput.deleteFile(file);
615
616 }
617 }
618 }
619 if (file.exists()) {
620 return file;
621 }
622 return null;
623 }
624
625 public static File getTomcatSrcZipFile() {
626 final String serverInfo = Parameters.getServletContext().getServerInfo();
627 if (!serverInfo.matches("Apache Tomcat/\\d+\\.\\d+\\.\\d+")) {
628
629 return null;
630 }
631 final String version = serverInfo.substring(serverInfo.lastIndexOf('/') + 1);
632 final String fileName = "apache-tomcat-" + version + "-src.zip";
633 final File storageDirectory = Parameters
634 .getStorageDirectory(Parameters.getCurrentApplication());
635 final String subDirectory = "sources";
636 final File file = new File(storageDirectory, subDirectory + '/' + fileName);
637 if (!file.exists() || file.length() == 0) {
638 final String majorVersion = version.substring(0, version.indexOf('.'));
639 final String url = TOMCAT_ARCHIVES + "tomcat-" + majorVersion + "/v" + version + "/src/"
640 + fileName;
641 mkdirs(file.getParentFile());
642 try (OutputStream output = new FileOutputStream(file)) {
643 final LabradorRetriever labradorRetriever = new LabradorRetriever(new URL(url));
644 labradorRetriever.downloadTo(output);
645 } catch (final IOException e) {
646 InputOutput.deleteFile(file);
647 }
648 }
649 if (file.exists()) {
650 return file;
651 }
652 return null;
653 }
654
655 private static void mkdirs(File directory) {
656 if (!directory.exists() && !directory.mkdirs()) {
657 throw new IllegalStateException("Can't create directory " + directory.getPath());
658 }
659 }
660
661 private static byte[] readMavenFileFromJarFile(URL jarFileLocation, String pomFileName)
662 throws IOException {
663 try (ZipInputStream zipInputStream = new ZipInputStream(
664 new BufferedInputStream(jarFileLocation.openStream(), 4096))) {
665 ZipEntry entry = zipInputStream.getNextEntry();
666 while (entry != null) {
667 if (entry.getName().startsWith("META-INF/maven/")
668 && entry.getName().endsWith("/" + pomFileName)) {
669 return InputOutput.pumpToByteArray(zipInputStream);
670 }
671 zipInputStream.closeEntry();
672 entry = zipInputStream.getNextEntry();
673 }
674 }
675 return null;
676 }
677
678 private static List<String> getMavenRepositories() {
679 final String parameter = Parameter.MAVEN_REPOSITORIES.getValue();
680 if (parameter != null) {
681 final List<String> result = new ArrayList<>();
682 for (final String repo : parameter.split(",")) {
683 result.add(repo.trim());
684 }
685 return result;
686 }
687 return Arrays.asList(LOCAL_REPO.getPath(), MAVEN_CENTRAL);
688 }
689
690 private static String replaceProperty(String value, Map<String, String> properties) {
691 if (value != null && value.startsWith("${") && value.endsWith("}")) {
692 final String propertyName = value.substring("${".length(),
693 value.length() - "}".length());
694 final String propertyValue = properties.get(propertyName);
695 if (propertyValue != null) {
696 return propertyValue;
697 }
698 }
699 return value;
700 }
701
702 @Override
703 public String toString() {
704 return getClass().getSimpleName() + '[' + groupId + ':' + artifactId + ':' + version + ']';
705 }
706 }
707