blob: b606c7920e7cd6c53cd45c646e567421082419a6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.nbbuild;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.util.FileUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Scans for known modules.
* Precise algorithm summarized in issue #42681 and issue #58966.
* @author Jesse Glick
*/
final class ModuleListParser {
private static Map<File,Map<String,Entry>> SOURCE_SCAN_CACHE = new HashMap<File,Map<String,Entry>>();
private static Map<File,Map<String,Entry>> SUITE_SCAN_CACHE = new HashMap<File,Map<String,Entry>>();
private static Map<File,Entry> STANDALONE_SCAN_CACHE = new HashMap<File,Entry>();
private static Map<File,Map<String,Entry>> BINARY_SCAN_CACHE = new HashMap<File,Map<String,Entry>>();
/** Clear caches. Cf. #71130. */
public static void resetCaches() {
SOURCE_SCAN_CACHE.clear();
SUITE_SCAN_CACHE.clear();
STANDALONE_SCAN_CACHE.clear();
BINARY_SCAN_CACHE.clear();
}
/** Synch with org.netbeans.modules.apisupport.project.universe.ModuleList.FOREST: */
private static final String[] FOREST = {
/*root*/null,
"contrib",
"otherlicenses",
// do not scan in misc; any real modules would have been put in contrib
// Will there be other subtrees in the future (not using suites)?
};
/**
* Find all NBM projects in a root, possibly from cache.
*/
private static Map<String,Entry> scanNetBeansOrgSources(File root, Map<String,Object> properties, Project project) throws IOException {
Map<String,Entry> entries = SOURCE_SCAN_CACHE.get(root);
if (entries == null) {
// Similar to #62221: if just invoked from a module in standard clusters, only scan those clusters (faster):
Set<String> standardModules = new HashSet<>();
boolean doFastScan = false;
String basedir = (String) properties.get("basedir");
String clusterList = (String) properties.get("nb.clusters.list");
if (basedir != null) {
File basedirF = new File(basedir);
if (clusterList == null) {
String config = (String) properties.get("cluster.config");
if (config != null) {
clusterList = (String) properties.get("clusters.config." + config + ".list");
}
}
if (clusterList != null) {
StringTokenizer tok = new StringTokenizer(clusterList, ", ");
while (tok.hasMoreTokens()) {
String clusterName = tok.nextToken();
String moduleList = (String) properties.get(clusterName);
if (moduleList != null) {
// Hack to treat libs.junit4 as if it were in platform for purposes of building, yet build to another cluster.
if (clusterName.equals("nb.cluster.platform")) {
moduleList += ",libs.junit4";
} else if (clusterName.equals("nb.cluster.stableuc")) {
moduleList = moduleList.replace(",libs.junit4", "");
}
StringTokenizer tok2 = new StringTokenizer(moduleList, ", ");
while (tok2.hasMoreTokens()) {
String module = tok2.nextToken();
standardModules.add(module);
// doFastScan |= new File(root, module.replace('/', File.separatorChar)).equals(basedirF);
}
}
}
}
}
String p = (String) properties.get("netbeans.dest.dir"); // NOI18N
int hash = root.hashCode() * 7 + (p == null ? 1 : p.hashCode());
File scanCache = new File(System.getProperty("java.io.tmpdir"), "nb-scan-cache-" + String.format("%x", hash) + "-" + (doFastScan ? "standard" : "full") + ".ser");
if (scanCache.isFile()) {
if (project != null) {
project.log("Loading module list from " + scanCache);
}
try {
try (InputStream is = new FileInputStream(scanCache)) {
ObjectInput oi = new ObjectInputStream(new BufferedInputStream(is));
@SuppressWarnings("unchecked") Map<File,Long[]> timestampsAndSizes = (Map) oi.readObject();
boolean matches = true;
for (Map.Entry<File,Long[]> entry : timestampsAndSizes.entrySet()) {
File f = entry.getKey();
if (f.lastModified() != entry.getValue()[0] || f.length() != entry.getValue()[1]) {
if (project != null) {
project.log("Cache ignored due to modifications in " + f);
}
matches = false;
break;
}
}
if (doFastScan) {
@SuppressWarnings("unchecked") Set<String> storedStandardModules = (Set) oi.readObject();
if (!standardModules.equals(storedStandardModules)) {
Set<String> added = new TreeSet<>(standardModules);
added.removeAll(storedStandardModules);
Set<String> removed = new TreeSet<>(storedStandardModules);
removed.removeAll(standardModules);
project.log("Cache ignored due to changes in modules among standard clusters: + " + added + " - " + removed);
matches = false;
}
}
File myProjectXml = project.resolveFile("nbproject/project.xml");
if (myProjectXml.isFile() && !timestampsAndSizes.containsKey(myProjectXml)) {
project.log("Cache ignored since it has no mention of " + myProjectXml);
matches = false; // #118098
}
if (matches) {
@SuppressWarnings("unchecked") Map<String,Entry> _entries = (Map) oi.readObject();
entries = _entries;
if (project != null) {
project.log("Loaded modules: " + entries.keySet(), Project.MSG_DEBUG);
}
}
}
} catch (Exception x) {
if (project != null) {
project.log("Error loading " + scanCache + ": " + x, Project.MSG_WARN);
}
}
}
if (entries == null) {
entries = new HashMap<>();
Map<File,Long[]> timestampsAndSizes = new HashMap<>();
registerTimestampAndSize(new File(root, "nbbuild" + File.separatorChar + "cluster.properties"), timestampsAndSizes);
registerTimestampAndSize(new File(root, "nbbuild" + File.separatorChar + "build.properties"), timestampsAndSizes);
registerTimestampAndSize(new File(root, "nbbuild" + File.separatorChar + "user.build.properties"), timestampsAndSizes);
doFastScan = false;
if (doFastScan) {
if (project != null) {
project.log("Scanning for modules in " + root + " among standard clusters");
}
for (String module : standardModules) {
scanPossibleProject(new File(root, module.replace('/', File.separatorChar)), entries, properties, module, ModuleType.NB_ORG, project, timestampsAndSizes);
}
} else {
// Might be an extra module (e.g. something in contrib); need to scan everything.
if (project != null) {
project.log("Scanning for modules in " + root);
project.log("Quick scan mode disabled since " + basedir + " not among standard modules of " + root + " which are " + standardModules, Project.MSG_VERBOSE);
}
List<String> subDirs = new ArrayList<>();
subDirs.addAll(Arrays.asList(FOREST));
String allClusters = (String) properties.get("clusters.config.full.list");
if(allClusters == null) {
allClusters = "";
}
StringTokenizer tok = new StringTokenizer(allClusters, ", ");
while (tok.hasMoreTokens()) {
String clusterName = tok.nextToken();
String clusterDir = (String) properties.get(clusterName + ".dir");
if (subDirs.contains(clusterDir)) {
continue;
}
subDirs.add(clusterDir);
}
for (String tree : subDirs) {
File dir = tree == null ? root : new File(root, tree);
File[] kids = dir.listFiles();
if (kids == null) {
continue;
}
for (File kid : kids) {
if (!kid.isDirectory()) {
continue;
}
String name = kid.getName();
String path = tree == null ? name : tree + "/" + name;
scanPossibleProject(kid, entries, properties, path, ModuleType.NB_ORG, project, timestampsAndSizes);
}
}
}
if (project != null) {
project.log("Found modules: " + entries.keySet(), Project.MSG_VERBOSE);
project.log("Cache depends on files: " + timestampsAndSizes.keySet(), Project.MSG_DEBUG);
}
scanCache.getParentFile().mkdirs();
try (OutputStream os = new FileOutputStream(scanCache)) {
ObjectOutput oo = new ObjectOutputStream(os);
oo.writeObject(timestampsAndSizes);
if (doFastScan) {
oo.writeObject(standardModules);
}
oo.writeObject(entries);
oo.flush();
}
}
SOURCE_SCAN_CACHE.put(root, entries);
}
return entries;
}
private static void registerTimestampAndSize(File f, Map<File,Long[]> timestampsAndSizes) {
if (timestampsAndSizes != null) {
timestampsAndSizes.put(f, new Long[] {f.lastModified(), f.length()});
}
}
/**
* Check a single dir to see if it is an NBM project, and if so, register it.
*/
private static boolean scanPossibleProject(File dir, Map<String,Entry> entries, Map<String,Object> properties,
String path, ModuleType moduleType, Project project, Map<File,Long[]> timestampsAndSizes) throws IOException {
File nbproject = new File(dir, "nbproject");
File projectxml = new File(nbproject, "project.xml");
if (!projectxml.isFile()) {
return false;
}
registerTimestampAndSize(projectxml, timestampsAndSizes);
Document doc;
try {
doc = XMLUtil.parse(new InputSource(projectxml.toURI().toString()),
false, true, /*XXX*/null, null);
} catch (Exception e) { // SAXException, IOException (#60295: e.g. encoding problem in XML)
// Include \n so that following line can be hyperlinked
throw (IOException) new IOException("Error parsing project file\n" + projectxml + ": " + e.getMessage()).initCause(e);
}
Element typeEl = XMLUtil.findElement(doc.getDocumentElement(), "type", ParseProjectXml.PROJECT_NS);
if (!XMLUtil.findText(typeEl).equals("org.netbeans.modules.apisupport.project")) {
return false;
}
Element configEl = XMLUtil.findElement(doc.getDocumentElement(), "configuration", ParseProjectXml.PROJECT_NS);
Element dataEl = ParseProjectXml.findNBMElement(configEl, "data");
if (dataEl == null) {
if (project != null) {
project.log(projectxml.toString() + ": warning: module claims to be a NBM project but is missing <data xmlns=\"" + ParseProjectXml.NBM_NS3 + "\">; maybe an old NB 4.[01] project?", Project.MSG_WARN);
}
return false;
}
Element cnbEl = ParseProjectXml.findNBMElement(dataEl, "code-name-base");
String cnb = XMLUtil.findText(cnbEl);
if (moduleType == ModuleType.NB_ORG && project != null) {
String expectedDirName = abbreviate(cnb);
String actualDirName = dir.getName();
if (!actualDirName.equals(expectedDirName)) {
throw new IOException("Expected module to be in dir named " + expectedDirName + " but was actually found in dir named " + actualDirName);
}
}
// Clumsy but the best way I know of to evaluate properties.
Project fakeproj = new Project();
if (project != null) {
// Try to debug any problems in the following definitions (cf. #59849).
for(BuildListener bl: project.getBuildListeners()) {
fakeproj.addBuildListener(bl);
}
}
fakeproj.setBaseDir(dir); // in case ${basedir} is used somewhere
Property faketask = new Property();
faketask.setProject(fakeproj);
switch (moduleType) {
case NB_ORG:
// do nothing here
break;
case SUITE:
faketask.setFile(new File(nbproject, "private/suite-private.properties"));
faketask.execute();
faketask.setFile(new File(nbproject, "suite.properties"));
faketask.execute();
faketask.setFile(new File(fakeproj.replaceProperties("${suite.dir}/nbproject/private/platform-private.properties")));
faketask.execute();
faketask.setFile(new File(fakeproj.replaceProperties("${suite.dir}/nbproject/platform.properties")));
faketask.execute();
break;
case STANDALONE:
faketask.setFile(new File(nbproject, "private/platform-private.properties"));
faketask.execute();
faketask.setFile(new File(nbproject, "platform.properties"));
faketask.execute();
break;
default:
assert false : moduleType;
}
File privateProperties = new File(nbproject, "private/private.properties".replace('/', File.separatorChar));
registerTimestampAndSize(privateProperties, timestampsAndSizes);
faketask.setFile(privateProperties);
faketask.execute();
File projectProperties = new File(nbproject, "project.properties");
registerTimestampAndSize(projectProperties, timestampsAndSizes);
faketask.setFile(projectProperties);
faketask.execute();
faketask.setFile(null);
faketask.setName("module.jar.dir");
faketask.setValue("modules");
faketask.execute();
assert fakeproj.getProperty("module.jar.dir") != null : fakeproj.getProperties();
faketask.setName("module.jar.basename");
faketask.setValue(cnb.replace('.', '-') + ".jar");
faketask.execute();
faketask.setName("module.jar");
faketask.setValue(fakeproj.replaceProperties("${module.jar.dir}/${module.jar.basename}"));
faketask.execute();
String id = null;
switch (moduleType) {
case NB_ORG:
assert path != null;
// Find the associated cluster.
// first try direct mapping in nbbuild/netbeans/moduleCluster.properties
String[] clusterDir = { (String) properties.get(path + ".dir") };
if (clusterDir[0] != null) {
final String subStr = clusterDir[0].substring(clusterDir[0].lastIndexOf('/') + 1);
if (path.startsWith(subStr + "/")) {
id = path.substring(subStr.length() + 1);
}
clusterDir[0] = subStr;
} else {
id = SetCluster.findClusterAndId("cluster.dir", clusterDir, properties, path, faketask, "extra");
}
faketask.setName("cluster.dir");
faketask.setValue(clusterDir[0]);
faketask.execute();
faketask.setName("netbeans.dest.dir");
faketask.setValue(properties.get("netbeans.dest.dir"));
faketask.execute();
faketask.setName("cluster");
faketask.setValue(fakeproj.replaceProperties("${netbeans.dest.dir}/${cluster.dir}"));
faketask.execute();
break;
case SUITE:
assert path == null;
faketask.setName("suite.dir");
faketask.setValue(properties.get("suite.dir"));
faketask.execute();
faketask.setName("suite.build.dir");
faketask.setValue(fakeproj.replaceProperties("${suite.dir}/build"));
faketask.execute();
faketask.setName("cluster");
faketask.setValue(fakeproj.replaceProperties("${suite.build.dir}/cluster"));
faketask.execute();
break;
case STANDALONE:
assert path == null;
faketask.setName("build.dir");
faketask.setValue(fakeproj.replaceProperties("${basedir}/build"));
faketask.execute();
faketask.setName("cluster");
faketask.setValue(fakeproj.replaceProperties("${build.dir}/cluster"));
faketask.execute();
break;
default:
assert false : moduleType;
}
File jar = fakeproj.resolveFile(fakeproj.replaceProperties("${cluster}/${module.jar}"));
List<File> exts = new ArrayList<>();
for (Element ext : XMLUtil.findSubElements(dataEl)) {
if (!ext.getLocalName().equals("class-path-extension")) {
continue;
}
Element binaryOrigin = ParseProjectXml.findNBMElement(ext, "binary-origin");
File origBin = null;
if (binaryOrigin != null) {
String reltext = XMLUtil.findText(binaryOrigin);
String nball = (String) properties.get("nb_all");
if (nball != null) {
faketask.setName("nb_all");
faketask.setValue(nball);
faketask.execute();
}
fakeproj.setBaseDir(dir);
origBin = fakeproj.resolveFile(fakeproj.replaceProperties(reltext));
}
File resultBin = null;
if (origBin == null || !origBin.exists()) {
Element runtimeRelativePath = ParseProjectXml.findNBMElement(ext, "runtime-relative-path");
if (runtimeRelativePath == null) {
throw new IOException("Have malformed <class-path-extension> in " + projectxml);
}
String reltext = XMLUtil.findText(runtimeRelativePath);
if (reltext != null) {
// No need to evaluate property refs in it - it is *not* substitutable-text in the schema.
String nball = (String) properties.get("nb_all");
resultBin = new File(jar.getParentFile(), reltext.replace('/', File.separatorChar));
resultBin = fixFxRtJar(resultBin, nball);
}
}
if (origBin != null) {
exts.add(origBin);
}
if (resultBin != null) {
exts.add(resultBin);
}
}
List<String> prereqs = new ArrayList<>();
List<String> rundeps = new ArrayList<>();
Element depsEl = ParseProjectXml.findNBMElement(dataEl, "module-dependencies");
if (depsEl == null) {
throw new IOException("Malformed project file " + projectxml);
}
Element testDepsEl = ParseProjectXml.findNBMElement(dataEl,"test-dependencies");
//compileDeps = Collections.emptyList();
Map<String,String[]> compileTestDeps = new HashMap<>();
if (testDepsEl != null) {
for (Element depssEl : XMLUtil.findSubElements(testDepsEl)) {
String testtype = ParseProjectXml.findTextOrNull(depssEl,"name") ;
if (testtype == null) {
throw new IOException("Must declare <name>unit</name> (e.g.) in <test-type> in " + projectxml);
}
List<String> compileDepsList = new ArrayList<>();
for (Element dep : XMLUtil.findSubElements(depssEl)) {
if (dep.getTagName().equals("test-dependency")) {
if (ParseProjectXml.findNBMElement(dep,"test") != null) {
compileDepsList.add(ParseProjectXml.findTextOrNull(dep, "code-name-base"));
}
}
}
compileTestDeps.put(testtype, compileDepsList.toArray(new String[0]));
}
}
for (Element dep : XMLUtil.findSubElements(depsEl)) {
Element cnbEl2 = ParseProjectXml.findNBMElement(dep, "code-name-base");
if (cnbEl2 == null) {
throw new IOException("Malformed project file " + projectxml);
}
String cnb2 = XMLUtil.findText(cnbEl2);
rundeps.add(cnb2);
if (ParseProjectXml.findNBMElement(dep, "build-prerequisite") == null) {
continue;
}
prereqs.add(cnb2);
}
String cluster = fakeproj.getProperty("cluster.dir"); // may be null
Entry entry = new Entry(cnb, jar, exts.toArray(new File[exts.size()]), dir, path, id == null ? path : id,
prereqs.toArray(new String[prereqs.size()]),
cluster,
rundeps.toArray(new String[rundeps.size()]),
compileTestDeps,
new File(dir, "module-auto-deps.xml")
);
if (entries.containsKey(cnb)) {
throw new IOException("Duplicated module " + cnb + ": found in " + entries.get(cnb) + " and " + entry);
} else {
entries.put(cnb, entry);
}
return true;
}
private static File fixFxRtJar(File resultBin, String nball) {
final String path = resultBin.getPath().replace(File.separatorChar, '/');
if (!resultBin.exists() && path.contains("${java.home}/lib/ext/jfxrt.jar")) {
String jhm = System.getProperty("java.home");
resultBin = new File(new File(new File(new File(jhm), "lib"), "ext"), "jfxrt.jar");
if (!resultBin.exists()) {
File jdk7 = new File(new File(new File(jhm), "lib"), "jfxrt.jar");
if (jdk7.exists()) {
resultBin = jdk7;
} else if (nball != null) {
File external = new File(new File(new File(new File(nball), "libs.javafx"), "external"), "jfxrt.jar");
if (external.exists()) {
resultBin = external;
}
}
}
}
return resultBin;
}
static String abbreviate(String cnb) {
return cnb.replaceFirst("^org\\.netbeans\\.modules\\.", ""). // NOI18N
replaceFirst("^org\\.netbeans\\.(libs|lib|api|spi|core)\\.", "$1."). // NOI18N
replaceFirst("^org\\.netbeans\\.", "o.n."). // NOI18N
replaceFirst("^org\\.openide\\.", "openide."). // NOI18N
replaceFirst("^org\\.", "o."). // NOI18N
replaceFirst("^com\\.sun\\.", "c.s."). // NOI18N
replaceFirst("^com\\.", "c."); // NOI18N
}
/**
* Find all modules in a binary build, possibly from cache.
*/
private static Map<String,Entry> scanBinaries(Project project, File[] clusters) throws IOException {
Map<String,Entry> allEntries = new HashMap<>();
for (File cluster : clusters) {
Map<String, Entry> entries = BINARY_SCAN_CACHE.get(cluster);
if (entries == null) {
if (project != null) {
project.log("Scanning for modules in " + cluster);
}
entries = new HashMap<>();
doScanBinaries(cluster, entries);
if (project != null) {
project.log("Found modules: " + entries.keySet(), Project.MSG_VERBOSE);
}
BINARY_SCAN_CACHE.put(cluster, entries);
}
allEntries.putAll(entries);
}
return allEntries;
}
private static final String[] MODULE_DIRS = {
"modules",
"modules/eager",
"modules/autoload",
"lib",
"core",
};
/**
* Look for all possible modules in a NB build.
* Checks modules/{,autoload/,eager/}*.jar as well as well-known core/*.jar and lib/boot.jar in each cluster.
* XXX would be slightly more precise to check config/Modules/*.xml rather than scan for module JARs.
*/
private static void doScanBinaries(File cluster, Map<String,Entry> entries) throws IOException {
File moduleAutoDepsDir = new File(new File(cluster, "config"), "ModuleAutoDeps");
for (String moduleDir : MODULE_DIRS) {
if (moduleDir.endsWith("lib") && !cluster.getName().contains("platform")) {
continue;
}
File dir = new File(cluster, moduleDir.replace('/', File.separatorChar));
if (!dir.isDirectory()) {
continue;
}
File[] jars = dir.listFiles();
if (jars == null) {
throw new IOException("Cannot examine dir " + dir);
}
for (File m : jars) {
scanOneBinary(m, cluster, entries, moduleAutoDepsDir);
}
}
final File configDir = new File(new File(cluster, "config"), "Modules");
File[] configs = configDir.listFiles();
XPathExpression expr = null;
DocumentBuilder b = null;
if (configs != null) {
for (File xml : configs) {
// TODO, read location, scan
final String fileName = xml.getName();
if (!fileName.endsWith(".xml")) {
continue;
}
if (entries.containsKey(fileName.substring(0, fileName.length() - 4).replace('-', '.'))) {
continue;
}
try {
if (expr == null) {
expr = XPathFactory.newInstance().newXPath().compile("/module/param[@name='jar']");
b = DocumentBuilderFactory.newInstance().newDocumentBuilder();
b.setEntityResolver(new EntityResolver() {
public InputSource resolveEntity(String publicId, String systemId)
throws SAXException, IOException {
return new InputSource(new ByteArrayInputStream(new byte[0]));
}
});
}
Document doc = b.parse(xml);
String res = expr.evaluate(doc);
File jar = new File(cluster, res.replace('/', File.separatorChar));
if (!jar.isFile()) {
if (cluster.getName().equals("ergonomics")) {
// this is normal
continue;
}
throw new BuildException("Cannot find module " + jar + " from " + xml);
}
scanOneBinary(jar, cluster, entries, moduleAutoDepsDir);
} catch (Exception ex) {
throw new BuildException(ex);
}
}
}
}
private static Map<String,Entry> scanSuiteSources(Map<String,Object> properties, Project project) throws IOException {
File basedir = new File((String) properties.get("basedir"));
String suiteDir = (String) properties.get("suite.dir");
if (suiteDir == null) {
throw new IOException("No definition of suite.dir in " + basedir);
}
File suite = FileUtils.getFileUtils().resolveFile(basedir, suiteDir);
if (!suite.isDirectory()) {
throw new IOException("No such suite " + suite);
}
Map<String,Entry> entries = SUITE_SCAN_CACHE.get(suite);
if (entries == null) {
if (project != null) {
project.log("Scanning for modules in suite " + suite);
}
entries = new HashMap<>();
doScanSuite(entries, suite, properties, project);
if (project != null) {
project.log("Found modules: " + entries.keySet(), Project.MSG_VERBOSE);
}
SUITE_SCAN_CACHE.put(suite, entries);
}
return entries;
}
private static void doScanSuite(Map<String,Entry> entries, File suite, Map<String,Object> properties, Project project) throws IOException {
Project fakeproj = new Project();
fakeproj.setBaseDir(suite); // in case ${basedir} is used somewhere
Property faketask = new Property();
faketask.setProject(fakeproj);
faketask.setFile(new File(suite, "nbproject/private/private.properties".replace('/', File.separatorChar)));
faketask.execute();
faketask.setFile(new File(suite, "nbproject/project.properties".replace('/', File.separatorChar)));
faketask.execute();
String modulesS = fakeproj.getProperty("modules");
if (modulesS == null) {
throw new IOException("No definition of modules in " + suite);
}
String[] modules = Path.translatePath(fakeproj, modulesS);
for (int i = 0; i < modules.length; i++) {
File module = new File(modules[i]);
if (!module.isDirectory()) {
throw new IOException("No such module " + module + " referred to from " + suite);
}
if (!scanPossibleProject(module, entries, properties, null, ModuleType.SUITE, project, null)) {
throw new IOException("No valid module found in " + module + " referred to from " + suite);
}
}
}
private static Entry scanStandaloneSource(Map<String,Object> properties, Project project) throws IOException {
if (properties.get("project") == null) return null; //Not a standalone module
File basedir = new File((String) properties.get("project"));
Entry entry = STANDALONE_SCAN_CACHE.get(basedir);
if (entry == null) {
Map<String,Entry> entries = new HashMap<>();
if (!scanPossibleProject(basedir, entries, properties, null, ModuleType.STANDALONE, project, null)) {
throw new IOException("No valid module found in " + basedir);
}
assert entries.size() == 1;
entry = entries.values().iterator().next();
STANDALONE_SCAN_CACHE.put(basedir, entry);
}
return entry;
}
/** all module entries, indexed by cnb */
private final Map<String,Entry> entries;
/**
* Initiates scan if not already parsed.
* Properties interpreted:
* <ol>
* <li> ${nb_all} - location of NB sources (used only for netbeans.org modules)
* <li> ${netbeans.dest.dir} - location of NB build (used only for NB.org modules)
* <li> ${cluster.path.final} - location of clusters to build against,
* created from ${cluster.path} specified in platform.properties file (used only for suite and standalone modules)
* <li> ${basedir} - directory of the project initiating the scan (most significant for standalone modules)
* <li> ${suite.dir} - directory of the suite (used only for suite modules)
* <li> ${nb.cluster.TOKEN} - list of module paths included in cluster TOKEN (comma-separated) (used only for netbeans.org modules)
* <li> ${nb.cluster.TOKEN.dir} - directory in ${netbeans.dest.dir} where cluster TOKEN is built (used only for netbeans.org modules)
* <li> ${project} - basedir for standalone modules
* </ol>
* @param properties some properties to be used (see above)
* @param type the type of project
* @param project a project ref, only for logging (may be null with no loss of semantics)
*/
public ModuleListParser(Map<String,Object> properties, ModuleType type, Project project) throws IOException {
String nball = (String) properties.get("nb_all");
File basedir = new File((String) properties.get("basedir"));
final FileUtils fu = FileUtils.getFileUtils();
if (type != ModuleType.NB_ORG) {
// add extra clusters
String suiteDirS = (String) properties.get("suite.dir");
boolean hasSuiteDir = suiteDirS != null && suiteDirS.length() > 0;
String clusterPath = (String) properties.get("cluster.path.final");
File[] clusters = null;
if (clusterPath != null) {
String[] clustersS;
if (hasSuiteDir) {
// resolve suite modules against fake suite project
Project fakeproj = new Project();
fakeproj.setBaseDir(new File(suiteDirS));
clustersS = Path.translatePath(fakeproj, clusterPath);
} else {
clustersS = Path.translatePath(project, clusterPath);
}
clusters = new File[clustersS.length];
if (clustersS != null && clustersS.length > 0) {
for (int j = 0; j < clustersS.length; j++) {
File cluster = new File(clustersS[j]);
if (! cluster.isDirectory()) {
throw new IOException("No such cluster " + cluster + " referred to from ${cluster.path.final}: " + clusterPath);
}
clusters[j] = cluster;
}
}
}
if (clusters == null || clusters.length == 0)
throw new IOException("Invalid ${cluster.path.final}: " + clusterPath);
// External module.
if (nball != null && project != null) {
project.log("You must *not* declare <suite-component/> or <standalone/> for a netbeans.org module in " + basedir + "; fix project.xml to use the /2 schema", Project.MSG_WARN);
}
entries = scanBinaries(project, clusters);
if (type == ModuleType.SUITE) {
entries.putAll(scanSuiteSources(properties, project));
} else {
assert type == ModuleType.STANDALONE;
Entry e = scanStandaloneSource(properties, project);
entries.put(e.getCnb(), e);
}
} else {
// netbeans.org module.
String buildS = (String) properties.get("netbeans.dest.dir");
if (buildS == null) {
throw new IOException("No definition of netbeans.dest.dir in " + basedir);
}
// Resolve against basedir, and normalize ../ sequences and so on in case they are used.
// Neither operation is likely to be needed, but just in case.
File build = fu.normalize(fu.resolveFile(basedir, buildS).getAbsolutePath());
if (nball == null) {
throw new IOException("You must declare either <suite-component/> or <standalone/> for an external module in " + new File((String) properties.get("basedir")));
}
String nbBuildDir = (String) properties.get("nb.build.dir");
if (!build.equals(new File(new File(nball, "nbbuild"), "netbeans")) && !(nbBuildDir != null && build.equals(new File(nbBuildDir, "netbeans")))) {
// Potentially orphaned module to be built against specific binaries, plus perhaps other source deps.
if (!build.isDirectory()) {
throw new IOException("No such netbeans.dest.dir: " + build);
}
// expand clusters in build
File[] clusters = build.listFiles();
if (clusters == null) {
throw new IOException("Cannot examine dir " + build);
}
entries = scanBinaries(project, clusters);
// Add referenced module in case it does not appear otherwise.
Entry e = scanStandaloneSource(properties, project);
if (e != null) {
entries.put(e.getCnb(), e);
}
entries.putAll(scanNetBeansOrgSources(new File(nball), properties, project));
} else {
entries = scanNetBeansOrgSources(new File(nball), properties, project);
}
}
}
/**
* Find all entries in this list.
* @return a set of all known entries
*/
public Set<Entry> findAll() {
return new HashSet<>(entries.values());
}
/**
* Find one entry by code name base.
* @param cnb the desired code name base
* @return the matching entry or null
*/
public Entry findByCodeNameBase(String cnb) {
return entries.get(cnb);
}
/** parse Openide-Module-Module-Dependencies entry
* @return array of code name bases
*/
private static String[] parseRuntimeDependencies(String moduleDependencies) {
if (moduleDependencies == null) {
return new String[0];
}
List<String> cnds = new ArrayList<>();
StringTokenizer toks = new StringTokenizer(moduleDependencies,",");
while (toks.hasMoreTokens()) {
String token = toks.nextToken().trim();
// substring cnd/x
int slIdx = token.indexOf('/');
if (slIdx != -1) {
token = token.substring(0,slIdx);
}
// substring cnd' 'xx
slIdx = token.indexOf(' ');
if (slIdx != -1) {
token = token.substring(0,slIdx);
}
// substring cnd >
slIdx = token.indexOf('>');
if (slIdx != -1) {
token = token.substring(0,slIdx);
}
token = token.trim();
if (token.length() > 0) {
cnds.add(token);
}
}
return cnds.toArray(new String[cnds.size()]);
}
static boolean scanOneBinary(File m, File cluster, Map<String, Entry> entries, File moduleAutoDepsDir) throws IOException {
if (!m.getName().endsWith(".jar")) {
return true;
}
JarFile jf;
try {
jf = new JarFile(m);
} catch (IOException x) {
throw new IOException("could not open " + m + ": " + x, x);
}
File dir = m.getParentFile();
try {
Attributes attr = jf.getManifest().getMainAttributes();
String codename = JarWithModuleAttributes.extractCodeName(attr);
if (codename == null) {
return true;
}
String codenamebase;
int slash = codename.lastIndexOf('/');
if (slash == -1) {
codenamebase = codename;
} else {
codenamebase = codename.substring(0, slash);
}
String cp = attr.getValue("Class-Path");
File[] exts;
if (cp == null) {
exts = new File[0];
} else {
String[] pieces = cp.split(" +");
exts = new File[pieces.length];
for (int l = 0; l < pieces.length; l++) {
String append = URLDecoder.decode(pieces[l].replace('/', File.separatorChar), "UTF-8");
exts[l] = fixFxRtJar(new File(dir, append), null);
}
}
String moduleDependencies = attr.getValue("OpenIDE-Module-Module-Dependencies");
Entry entry = new Entry(codenamebase, m, exts, null, null, null, null, cluster.getName(),
parseRuntimeDependencies(moduleDependencies), Collections.<String,String[]>emptyMap(),
new File(moduleAutoDepsDir, codenamebase.replace('.', '-') + ".xml"));
Entry prev = entries.put(codenamebase, entry);
if (prev != null && !prev.equals(entry)) {
throw new IOException("Duplicated module " + codenamebase + ": found in " + prev + " and " + entry);
}
} finally {
jf.close();
}
return false;
}
/**
* One entry in the file.
*/
@SuppressWarnings("serial") // really want it to be incompatible if format changes
public static final class Entry implements Serializable {
private final String cnb;
private final File jar;
private final File[] classPathExtensions;
private final File sourceLocation;
private final String netbeansOrgPath;
private final String netbeansOrgId;
private final String[] buildPrerequisites;
private final String clusterName;
private final String[] runtimeDependencies;
// dependencies on other tests
private final Map<String,String[]> testDependencies;
private final File moduleAutoDeps;
Entry(String cnb, File jar, File[] classPathExtensions, File sourceLocation, String netbeansOrgPath, String netbeansOrgId,
String[] buildPrerequisites, String clusterName,String[] runtimeDependencies, Map<String,String[]> testDependencies, File moduleAutoDeps) {
this.cnb = cnb;
this.jar = jar;
this.classPathExtensions = classPathExtensions;
this.sourceLocation = sourceLocation;
this.netbeansOrgPath = netbeansOrgPath;
this.netbeansOrgId = netbeansOrgId;
this.buildPrerequisites = buildPrerequisites;
this.clusterName = clusterName;
this.runtimeDependencies = runtimeDependencies;
assert testDependencies != null;
this.testDependencies = testDependencies;
this.moduleAutoDeps = moduleAutoDeps;
}
/**
* Get the code name base, e.g. org.netbeans.modules.ant.grammar.
*/
public String getCnb() {
return cnb;
}
/**
* Get the absolute JAR location, e.g. .../ide/modules/org-netbeans-modules-ant-grammar.jar.
*/
public File getJar() {
return jar;
}
/**
* Get a list of extensions to the class path of this module (may be empty).
*/
public File[] getClassPathExtensions() {
return classPathExtensions;
}
/**
* @return the sourceLocation, may be null.
*/
public File getSourceLocation() {
return sourceLocation;
}
/**
* Get the path within netbeans.org, if this is a netbeans.org module (else null).
*/
public String getNetbeansOrgPath() {
return netbeansOrgPath;
}
/**
* Get a list of declared build prerequisites (or null for sourceless entries).
* Each entry is a code name base.
*/
public String[] getBuildPrerequisites() {
return buildPrerequisites;
}
/** Get runtime dependencies, OpenIDE-Module-Dependencies entry.
* Each entry is a code name base.
*/
public String[] getRuntimeDependencies() {
return runtimeDependencies;
}
/**
* Return the name of the cluster in which this module resides.
* If this entry represents an external module in source form,
* then the cluster will be null. If the module represents a netbeans.org
* module or a binary module in a platform, then the cluster name will
* be the (base) name of the directory containing the "modules" subdirectory
* (sometimes "lib" or "core") where the JAR is.
*/
public String getClusterName() {
return clusterName;
}
public Map<String,String[]> getTestDependencies() {
return testDependencies;
}
/**
* Gets the module auto dependencies XML file.
* @return a file location (may or may not exist)
*/
public File getModuleAutoDeps() {
return moduleAutoDeps;
}
public @Override String toString() {
return (sourceLocation != null ? sourceLocation : jar).getAbsolutePath();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Entry other = (Entry) obj;
if ((this.cnb == null) ? (other.cnb != null) : !this.cnb.equals(other.cnb)) {
return false;
}
if (this.jar != other.jar && (this.jar == null || !this.jar.equals(other.jar))) {
return false;
}
if (!Arrays.deepEquals(this.classPathExtensions, other.classPathExtensions)) {
return false;
}
if (this.sourceLocation != other.sourceLocation && (this.sourceLocation == null || !this.sourceLocation.equals(other.sourceLocation))) {
return false;
}
if ((this.netbeansOrgPath == null) ? (other.netbeansOrgPath != null) : !this.netbeansOrgPath.equals(other.netbeansOrgPath)) {
return false;
}
if ((this.netbeansOrgId == null) ? (other.netbeansOrgId != null) : !this.netbeansOrgId.equals(other.netbeansOrgId)) {
return false;
}
if (!Arrays.deepEquals(this.buildPrerequisites, other.buildPrerequisites)) {
return false;
}
if ((this.clusterName == null) ? (other.clusterName != null) : !this.clusterName.equals(other.clusterName)) {
return false;
}
if (!Arrays.deepEquals(this.runtimeDependencies, other.runtimeDependencies)) {
return false;
}
if (this.testDependencies != other.testDependencies && (this.testDependencies == null || !this.testDependencies.equals(other.testDependencies))) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 5;
hash = 83 * hash + (this.cnb != null ? this.cnb.hashCode() : 0);
hash = 83 * hash + (this.jar != null ? this.jar.hashCode() : 0);
hash = 83 * hash + Arrays.deepHashCode(this.classPathExtensions);
hash = 83 * hash + (this.sourceLocation != null ? this.sourceLocation.hashCode() : 0);
hash = 83 * hash + (this.netbeansOrgPath != null ? this.netbeansOrgPath.hashCode() : 0);
hash = 83 * hash + (this.netbeansOrgId != null ? this.netbeansOrgId.hashCode() : 0);
hash = 83 * hash + Arrays.deepHashCode(this.buildPrerequisites);
hash = 83 * hash + (this.clusterName != null ? this.clusterName.hashCode() : 0);
hash = 83 * hash + Arrays.deepHashCode(this.runtimeDependencies);
hash = 83 * hash + (this.testDependencies != null ? this.testDependencies.hashCode() : 0);
return hash;
}
String getNetbeansOrgId() {
return netbeansOrgId;
}
}
}