package aQute.maven.provider; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import aQute.bnd.version.MavenVersion; import aQute.lib.io.IO; import aQute.lib.strings.Strings; import aQute.maven.api.Archive; import aQute.maven.api.IPom; import aQute.maven.api.MavenScope; import aQute.maven.api.Program; import aQute.maven.api.Revision; /** * Parser and placeholder for POM information. */ public class POM implements IPom { static Logger l = LoggerFactory.getLogger(POM.class); static DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); static XPathFactory xpf = XPathFactory.newInstance(); private Revision revision; private String packaging; private final Properties properties; private final POM parent; private Map<Program,Dependency> dependencies = new LinkedHashMap<>(); private Map<Program,Dependency> dependencyManagement = new LinkedHashMap<>(); private XPath xp; private MavenRepository repo; private boolean ignoreParentIfAbsent; public static POM parse(MavenRepository repo, File file) throws Exception { try { return new POM(repo, file); } catch (Exception e) { l.error("Failed to parse POM file {}", file); throw e; } } public POM() { this.properties = System.getProperties(); this.parent = null; } public POM(MavenRepository repo, InputStream in) throws Exception { this(repo, getDocBuilder().parse(processEntities(in)), false); } public POM(MavenRepository mavenRepository, InputStream pomFile, boolean ignoreParentIfAbsent) throws SAXException, IOException, ParserConfigurationException, Exception { this(mavenRepository, getDocBuilder().parse(processEntities(pomFile)), ignoreParentIfAbsent); } private static DocumentBuilder getDocBuilder() throws ParserConfigurationException { DocumentBuilder db = dbf.newDocumentBuilder(); return db; } final static Pattern ENTITY_CLEAN_UP = Pattern.compile("&([-a-z0-9_]+);"); private static InputStream processEntities(InputStream in) throws IOException { byte[] read = IO.read(in); int l = read.length; outer: for (int i = 0; i < read.length; i++) { if (read[i] == '&') { StringBuilder sb = new StringBuilder(); for (int j = i + 1; j < read.length && read[j] != ';'; j++) { if (j > i + 10) continue outer; char c = (char) read[j]; if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) sb.append(c); else continue outer; } switch (sb.toString()) { case "lt" : case "gt" : case "amp" : case "quote" : case "apos" : break; default : read[i] = '?'; break; } } } return new ByteArrayInputStream(read); } public POM(MavenRepository repo, File file) throws Exception { this(repo, IO.stream(file)); } public POM(MavenRepository repo, Document doc) throws Exception { this(repo, doc, false); } public POM(MavenRepository repo, Document doc, boolean ignoreIfParentAbsent) throws Exception { this.repo = repo; this.ignoreParentIfAbsent = ignoreIfParentAbsent; xp = xpf.newXPath(); String parentGroup = Strings.trim(xp.evaluate("project/parent/groupId", doc)); String parentArtifact = Strings.trim(xp.evaluate("project/parent/artifactId", doc)); String parentVersion = Strings.trim(xp.evaluate("project/parent/version", doc)); String relativePath = Strings.trim(xp.evaluate("project/parent/relativePath", doc)); if (!parentGroup.isEmpty() && !parentArtifact.isEmpty() && !parentVersion.isEmpty()) { Program program = Program.valueOf(parentGroup, parentArtifact); if (program == null) throw new IllegalArgumentException("Invalid parent program " + parentGroup + ":" + parentArtifact); MavenVersion v = MavenVersion.parseMavenString(parentVersion); if (v == null) throw new IllegalArgumentException("Invalid version for parent pom " + program + ":" + v); File fp; if (relativePath != null && !relativePath.isEmpty() && (fp = IO.getFile(relativePath)).isFile()) { this.parent = new POM(repo, fp); } else { Revision revision = program.version(v); POM pom = null; try { pom = repo.getPom(revision); } catch (Exception e) { if (!this.ignoreParentIfAbsent) throw e; } if (pom == null) { if (this.ignoreParentIfAbsent) pom = new POM(); // not found else throw new IllegalArgumentException("No parent for pom " + pom); } this.parent = pom; } } else { this.parent = new POM(); } this.properties = new Properties(this.parent.properties); Element project = doc.getDocumentElement(); index(project, "project.", "modelVersion", "groupId", "artifactId", "version", "packaging"); NodeList props = (NodeList) xp.evaluate("properties", project, XPathConstants.NODESET); for (int i = 0; i < props.getLength(); i++) index((Element) props.item(i), ""); String group = get("project.groupId", parentGroup); String artifact = getNoInheritance("project.artifactId", null); String version = get("project.version", parentVersion); this.packaging = getNoInheritance("project.packaging", "jar"); Program program = Program.valueOf(group, artifact); if (program == null) throw new IllegalArgumentException("Invalid program for pom " + group + ":" + artifact); MavenVersion v = MavenVersion.parseMavenString(version); if (v == null) throw new IllegalArgumentException("Invalid version for pom " + group + ":" + artifact + ":" + version); this.revision = program.version(v); properties.put("pom.groupId", group); properties.put("pom.artifactId", artifact); properties.put("pom.version", version); if (parent.revision != null) properties.put("parent.version", parent.getVersion().toString()); else properties.put("parent.version", "parent version from " + revision + " but not parent?"); properties.put("version", version); properties.put("pom.currentVersion", version); properties.put("pom.packaging", this.packaging); NodeList dependencies = (NodeList) xp.evaluate("project/dependencies/dependency", doc, XPathConstants.NODESET); for (int i = 0; i < dependencies.getLength(); i++) { Element dependency = (Element) dependencies.item(i); Dependency d = dependency(dependency); this.dependencies.put(d.program, d); } NodeList dependencyManagement = (NodeList) xp.evaluate("project/dependencyManagement/dependencies/dependency", doc, XPathConstants.NODESET); for (int i = 0; i < dependencyManagement.getLength(); i++) { Element dependency = (Element) dependencyManagement.item(i); Dependency d = dependency(dependency); this.dependencyManagement.put(d.program, d); } xp = null; } private MavenVersion getVersion() { return revision.version; } private Dependency dependency(Element dependency) throws Exception { String groupId = get(dependency, "groupId", "<no group>"); String artifactId = get(dependency, "artifactId", "<no artifact>"); Dependency d = new Dependency(); d.optional = isTrue(get(dependency, "optional", "true")); String version = get(dependency, "version", null); String extension = get(dependency, "type", "jar"); String classifier = get(dependency, "classifier", null); String scope = get(dependency, "scope", "compile"); Program program = Program.valueOf(groupId, artifactId); if (program == null) throw new IllegalArgumentException( "Invalid dependency in " + revision + " to " + groupId + ":" + artifactId); d.program = program; d.version = version; d.type = extension; d.classifier = classifier; d.scope = MavenScope.getScope(scope); // TODO exclusions return d; } private Dependency getDirectDependency(Program program) { Dependency dependency = dependencies.get(program); if (dependency != null) return dependency; dependency = dependencyManagement.get(program); if (dependency != null) return dependency; if (parent != null) return parent.getDirectDependency(program); return null; } private boolean isTrue(String other) { return "true".equalsIgnoreCase(other); } private String get(Element dependency, String name, String deflt) throws XPathExpressionException { String value = xp.evaluate(name, dependency); if (value == null || value.isEmpty()) return Strings.trim(deflt); return Strings.trim(replaceMacros(value)); } private String get(String key, String deflt) { String value = properties.getProperty(key); if (value == null) value = deflt; if (value == null) return null; return replaceMacros(value); } private String getNoInheritance(String key, String deflt) { String value = (String) properties.get(key); if (value == null) value = deflt; return replaceMacros(value); } final static Pattern MACRO_P = Pattern.compile("\\$\\{(?<env>env\\.)?(?<key>[.a-z0-9$_-]+)\\}", Pattern.CASE_INSENSITIVE); private String replaceMacros(String value) { Matcher m = MACRO_P.matcher(value); StringBuffer sb = new StringBuffer(); while (m.find()) { String key = m.group("key"); if (m.group("env") != null) m.appendReplacement(sb, replaceMacros(System.getenv(key))); else { String property = this.properties.getProperty(key); if (property != null && property.indexOf('$') >= 0) property = replaceMacros(property); if (property == null) { System.out.println("?? prop: " + key); m.appendReplacement(sb, Matcher.quoteReplacement("${" + key + "}")); } else m.appendReplacement(sb, Matcher.quoteReplacement(property)); } } m.appendTail(sb); return sb.toString(); } private void index(Element node, String prefix, String... names) throws XPathExpressionException { String expr = "./*"; if (names.length > 0) { StringBuilder sb = new StringBuilder("./*["); String del = "name()="; for (String name : names) { sb.append(del).append('"').append(name).append('"'); del = " or name()="; } sb.append("]"); expr = sb.toString(); } NodeList childNodes = (NodeList) xp.evaluate(expr, node, XPathConstants.NODESET); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); String key = child.getNodeName(); String value = child.getTextContent().trim(); properties.put(prefix + key, value); } } public Revision getRevision() { return revision; } public String getPackaging() { return packaging; } public Archive binaryArchive() { return revision .archive( packaging == null || packaging.isEmpty() || packaging.equals("bundle") || packaging.equals("pom") || packaging.equals("eclipse-plugin") ? "jar" : packaging, null); } @Override public Map<Program,Dependency> getDependencies(MavenScope scope, boolean transitive) throws Exception { return getDependencies(EnumSet.of(scope), transitive); } public Map<Program,Dependency> getDependencies(EnumSet<MavenScope> scope, boolean transitive) throws Exception { Map<Program,Dependency> deps = new LinkedHashMap<>(); Set<Program> visited = new HashSet<>(); getDependencies(deps, scope, transitive, visited); return deps; } private void resolve(Dependency d) { if (d.version == null) { Dependency dependency = dependencyManagement.get(d.program); if (dependency != null && dependency.version != null) { d.version = dependency.version; return; } Dependency directDependency = parent.getDirectDependency(d.program); if (directDependency == null) { d.error = "Cannot resolve ..."; } else d.version = directDependency.version; } } private void getDependencies(Map<Program,Dependency> deps, EnumSet<MavenScope> scope, boolean transitive, Set<Program> visited) throws Exception { if (revision == null) return; if (!visited.add(revision.program)) return; if (parent != null) parent.getDependencies(deps, scope, transitive, visited); List<Dependency> breadthFirst = new ArrayList<>(); for (Map.Entry<Program,Dependency> e : dependencies.entrySet()) { Dependency d = e.getValue(); resolve(d); if (deps.containsKey(d.program)) continue; if (scope.contains(d.scope)) { d.bindToVersion(repo); deps.put(e.getKey(), d); if (transitive && d.scope.isTransitive()) breadthFirst.add(d); } } for (Dependency d : breadthFirst) try { POM pom = repo.getPom(d.getRevision()); if (pom == null) { continue; } pom.getDependencies(deps, scope, transitive, visited); } catch (Exception ee) { d.error = ee.toString(); } } @Override public IPom getParent() { return parent; } @Override public String toString() { return "POM[" + revision + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((revision == null) ? 0 : revision.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; POM other = (POM) obj; if (revision == null) { if (other.revision != null) return false; } else if (!revision.equals(other.revision)) return false; return true; } public boolean isPomOnly() { return "pom".equals(packaging); } }