/* * 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.
*/
/** * Converts a set of NetBeans modules into OSGi bundles. * Since there are some aspects of the translation that must be sensitive to * context in order to preserve as closely as possible the semantics of the * NetBeans module system, processing proceeds in two phases: * <ol> * <li>Each module in the input is opened and scanned for packages which it defines * (including in its {@code Class-Path} extensions), and for packages which it * (statically) refers to (not including packages it itself defines, or any * packages in the {@code java.*} namespace). {@code OpenIDE-Module-Hide-Classpath-Packages} * declarations are also tracked, and a list of packages which seem to be * exported from this module (if any) is collected. * <li>Each module in the input is reopened. Now it is actually converted to an * OSGi bundle. Package import and export information from the first phase is * used to decide how to represent this bundle's imports: * <ol> * <li>Packages hidden by this bundle, or one of its direct dependencies, are * never to be represented in the OSGi manifest. * <li>Packages exported by one of this bundle's dependencies can also be skipped, * since {@code Require-Bundle} will pick them all up. * <li>Packages in the {@code org.osgi.*} namespace can be imported. * <li>Packages from the known Java Platform API can be imported. * <li>All other packages can be dynamically imported. Perhaps these will be * available somehow at runtime, perhaps from the system bundle. * </ol> * </ol>
*/ publicclass MakeOSGi extends Task {
private File destdir; private List<ResourceCollection> modules = new ArrayList<>();
/** * Mandatory destination directory. Bundles will be created here.
*/ publicvoid setDestdir(File destdir) { this.destdir = destdir;
}
/** * Adds a set of module JARs. * It is permitted for them to be JARs anywhere on disk, * but it is best if they are in a cluster structure * with ../update_tracking/*.xml present * so that associated files can be included in the bundle.
*/ publicvoid add(ResourceCollection modules) { this.modules.add(modules);
}
staticclass Info { final Set<String> importedPackages = new TreeSet<>(); final Set<String> exportedPackages = new TreeSet<>(); final Set<String> hiddenPackages = new TreeSet<>(); final Set<String> hiddenSubpackages = new TreeSet<>();
}
public @Override void execute() throws BuildException { if (destdir == null) { thrownew BuildException("missing destdir");
}
List<File> jars = new ArrayList<>();
List<File> fragments = new ArrayList<>();
Map<String,Info> infos = new HashMap<>();
log("Prescanning JARs..."); for (ResourceCollection rc : modules) {
Iterator<?> it = rc.iterator(); while (it.hasNext()) {
File jar = ((FileResource) it.next()).getFile();
log("Prescanning " + jar, Project.MSG_VERBOSE); if (jar.getParentFile().getName().equals("locale")) {
fragments.add(jar); continue;
} try { try (JarFile jf = new JarFile(jar)) {
Info info = new Info();
String cnb = prescan(jf, info, this); if (cnb == null) {
log(jar + " does not appear to be either a module or a bundle; skipping", Project.MSG_WARN);
} elseif (SKIPPED_PSEUDO_MODULES.contains(cnb)) {
log("Skipping " + jar);
} elseif (infos.containsKey(cnb)) {
log(jar + " appears to not be the only module named " + cnb, Project.MSG_WARN);
} else {
infos.put(cnb, info);
jars.add(jar);
}
}
} catch (Exception x) { thrownew BuildException("Could not prescan " + jar + ": " + x, x, getLocation());
}
}
} for (File jar : jars) { try {
process(jar, infos);
} catch (Exception x) { thrownew BuildException("Could not process " + jar + ": " + x, x, getLocation());
}
} for (File jar : fragments) { try {
processFragment(jar);
} catch (Exception x) { thrownew BuildException("Could not process " + jar + ": " + x, x, getLocation());
}
}
}
static String prescan(JarFile module, Info info, Task task) throws Exception {
Manifest manifest = module.getManifest(); if (manifest == null) { returnnull;
}
Attributes attr = manifest.getMainAttributes();
String cnb = attr.getValue("OpenIDE-Module"); if ("org.netbeans.libs.osgi".equals(cnb)) { // Otherwise get e.g. CCE: org.netbeans.core.osgi.Activator cannot be cast to org.osgi.framework.BundleActivator return cnb;
}
Set<String> availablePackages = new TreeSet<>();
scanClasses(module, info.importedPackages, availablePackages, task);
File antlib = new File(module.getName().replaceFirst("([/\\\\])modules([/\\\\][^/\\\\]+)", "$1ant$1nblib$2")); if (antlib.isFile()) { // ant/nblib/org-netbeans-modules-debugger-jpda-ant.jar references com.sun.jdi.* packages. // AntBridge.MainClassLoader.findClass will refuse to load these, // since it is expected that the module loader, thus also AuxClassLoader, can load them. // So we need to DynamicImport-Package these packages so that will be true.
Set<String> antlibPackages = new HashSet<>(); try (JarFile antlibJF = new JarFile(antlib)) {
scanClasses(antlibJF, antlibPackages, new HashSet<>(), task);
} for (String antlibImport : antlibPackages) { if (!antlibImport.startsWith("org.apache.tools.") && !availablePackages.contains(antlibImport)) {
info.importedPackages.add(antlibImport);
}
}
} if (cnb != null) {
cnb = cnb.replaceFirst("/\\d+$", "");
String hide = attr.getValue("OpenIDE-Module-Hide-Classpath-Packages"); if (hide != null) { for (String piece : hide.split("[, ]+")) { if (piece.isEmpty()) { continue;
} if (piece.endsWith(".*")) {
info.hiddenPackages.add(piece.substring(0, piece.length() - ".*".length()));
} elseif (piece.endsWith(".**")) {
info.hiddenSubpackages.add(piece.substring(0, piece.length() - ".**".length()));
} else { thrownew IOException("Bad OpenIDE-Module-Hide-Classpath-Packages piece: " + piece);
}
}
}
String pp = attr.getValue("OpenIDE-Module-Public-Packages");
String implVersion = attr.getValue("OpenIDE-Module-Implementation-Version"); if (implVersion != null && implVersion.matches("\\d+") && /* not just e.g. 201005242201 */ attr.getValue("OpenIDE-Module-Build-Version") != null) { // Since we have no idea who might be using these packages, have to make everything public.
info.exportedPackages.addAll(availablePackages);
pp = null;
} if (pp != null && !pp.equals("-")) { for (String p : pp.split("[, ]+")) { if (p.isEmpty()) { continue;
} if (p.endsWith(".*")) {
info.exportedPackages.add(p.substring(0, p.length() - ".*".length()));
} else { if (!p.endsWith(".**")) { thrownew IllegalArgumentException("Invalid package export: " + p);
} for (String actual : availablePackages) { if (actual.equals(p.substring(0, p.length() - ".**".length()))
|| actual.startsWith(p.substring(0, p.length() - "**".length()))) {
info.exportedPackages.add(actual);
}
}
}
}
}
} else { // #180201
cnb = JarWithModuleAttributes.extractCodeName(attr); if (cnb == null) { returnnull;
}
String exportPackage = attr.getValue("Export-Package"); if (exportPackage != null) { // http://stackoverflow.com/questions/1757065/java-splitting-a-comma-separated-string-but-ignoring-commas-in-quotes for (String piece : exportPackage.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")) {
info.exportedPackages.add(piece.trim().replaceFirst(";.+", ""));
}
}
} return cnb;
}
private File findDestFile(String bundleName, String bundleVersion) throws IOException {
File destFile = new File(destdir, bundleName + (bundleVersion != null ? "-" + bundleVersion : "") + ".jar"); for (File stale : destdir.listFiles()) { if (stale.getName().matches("\\Q" + bundleName + "\\E(-.+)?[.]jar") && !stale.equals(destFile)) {
log("Deleting copy under old name: " + stale); if (!stale.delete()) { thrownew IOException("Could not delete: " + stale);
}
}
} return destFile;
}
privatevoid process(File module, Map<String,Info> infos) throws Exception { try (JarFile jar = new JarFile(module)) {
Manifest netbeans = jar.getManifest();
Attributes netbeansAttr = netbeans.getMainAttributes(); if (netbeansAttr.getValue("Bundle-SymbolicName") != null) { // #180201
File bundleFile = findDestFile(JarWithModuleAttributes.extractCodeName(netbeansAttr), netbeansAttr.getValue("Bundle-Version")); if (bundleFile.lastModified() > module.lastModified()) {
log("Skipping " + module + " since " + bundleFile + " is newer", Project.MSG_VERBOSE); return;
}
Copy copy = new Copy();
copy.setProject(getProject());
copy.setOwningTarget(getOwningTarget());
copy.setFile(module);
copy.setTofile(bundleFile);
copy.setVerbose(true);
copy.execute(); return;
}
Manifest osgi = new Manifest();
Attributes osgiAttr = osgi.getMainAttributes();
translate(netbeansAttr, osgiAttr, infos);
String cnb = osgiAttr.getValue("Bundle-SymbolicName").replaceFirst(";.+", "");
File bundleFile = findDestFile(cnb, osgiAttr.getValue("Bundle-Version")); if (bundleFile.lastModified() > module.lastModified()) {
log("Skipping " + module + " since " + bundleFile + " is newer", Project.MSG_VERBOSE); return;
}
log("Processing " + module + " into " + bundleFile);
String dynamicImports = osgiAttr.getValue("DynamicImport-Package"); if (dynamicImports != null) {
log(cnb + " has imports of no known origin: " + dynamicImports, Project.MSG_WARN);
log("(you may need to define org.osgi.framework.system.packages.extra in your OSGi container)");
}
Properties localizedStrings = new Properties();
String locbundle = netbeansAttr.getValue("OpenIDE-Module-Localizing-Bundle"); if (locbundle != null) { try (InputStream is = jar.getInputStream(jar.getEntry(locbundle))) {
localizedStrings.load(is);
}
osgiAttr.putValue("Bundle-Localization", locbundle.replaceFirst("[.]properties$", ""));
}
handleDisplayAttribute(localizedStrings, netbeansAttr, osgiAttr, "OpenIDE-Module-Name", "Bundle-Name");
handleDisplayAttribute(localizedStrings, netbeansAttr, osgiAttr, "OpenIDE-Module-Display-Category", "Bundle-Category");
handleDisplayAttribute(localizedStrings, netbeansAttr, osgiAttr, "OpenIDE-Module-Short-Description", "Bundle-Description");
Map<String,File> bundledFiles = findBundledFiles(module, cnb); // XXX any use for OpenIDE-Module-Long-Description?
String classPath = netbeansAttr.getValue("Class-Path"); if (classPath != null) {
StringBuilder bundleCP = new StringBuilder(); for (String rawEntry : classPath.split("[, ]+")) {
String entry = URLDecoder.decode(rawEntry, "UTF-8"); if (entry.startsWith("${java.home}")) { continue;
}
String clusterPath = new URI(module.getParentFile().getName() + "/" + entry).normalize().toString(); if (bundledFiles.containsKey(clusterPath)) {
bundleCP.append("/OSGI-INF/files/").append(clusterPath).append(",");
} else {
log("Class-Path entry " + entry + " from " + module + " does not correspond to any apparent cluster file", Project.MSG_WARN);
}
}
osgiAttr.putValue("Bundle-Classpath", bundleCP + ".");
}
StringBuilder execFiles = null; for (Map.Entry<String,File> bundledFile : bundledFiles.entrySet()) { // XXX would be better to use ${nbm.executable.files} as specified by project // (since building bundles on Windows will never set this even if running on Unix) // but this information is available only during the project's build or in its NBM if (bundledFile.getValue().canExecute()) {
String name = bundledFile.getKey(); if (execFiles == null) {
execFiles = new StringBuilder(name);
} else {
execFiles.append(',').append(name);
}
}
} if (execFiles != null) {
osgiAttr.putValue("NetBeans-Executable-Files", execFiles.toString());
} // XXX modules/lib/*.dll/so => Bundle-NativeCode (but syntax is rather complex) try (OutputStream bundle = new FileOutputStream(bundleFile);
ZipOutputStream zos = new JarOutputStream(bundle, osgi)) {
Set<String> parents = new HashSet<>();
Enumeration<? extends ZipEntry> entries = jar.entries(); while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String path = entry.getName(); if (path.endsWith("/") || path.equals("META-INF/MANIFEST.MF")) { continue;
} try (InputStream is = jar.getInputStream(entry)) {
writeEntry(zos, path, is, parents);
}
} for (Map.Entry<String,File> bundledFile : bundledFiles.entrySet()) { try (InputStream is = new FileInputStream(bundledFile.getValue())) {
writeEntry(zos, "OSGI-INF/files/" + bundledFile.getKey(), is, parents);
}
}
zos.finish();
}
}
}
/** * Translate NetBeans module metadata to OSGi equivalents. * @param netbeans manifest header to be read * @param osgi manifest header to be written * @param infos information about imported and exported packages among all processed JARs
*/ void translate(Attributes netbeans, Attributes osgi, Map<String,Info> infos) throws Exception {
osgi.putValue("Manifest-Version", "1.0"); // workaround for JDK bug
osgi.putValue("Bundle-ManifestVersion", "2");
String codename = netbeans.getValue("OpenIDE-Module"); if (codename == null) { thrownew IllegalArgumentException("Does not appear to be a NetBeans module");
}
String cnb = codename.replaceFirst("/\\d+$", "");
osgi.putValue("Bundle-SymbolicName", cnb); if (cnb.equals("org.netbeans.core.osgi")) {
osgi.putValue("Bundle-Activator", "org.netbeans.core.osgi.Activator");
}
Info myInfo = infos.get(cnb);
String spec = netbeans.getValue("OpenIDE-Module-Specification-Version");
String bundleVersion = null; if (spec != null) {
bundleVersion = threeDotsWithMajor(spec, codename);
String buildVersion = netbeans.getValue("OpenIDE-Module-Build-Version"); if (buildVersion == null) {
buildVersion = netbeans.getValue("OpenIDE-Module-Implementation-Version");
} if (buildVersion != null) {
bundleVersion += "." + buildVersion.replaceAll("[^a-zA-Z0-9_-]", "_");
}
osgi.putValue("Bundle-Version", bundleVersion);
} // OpenIDE-Module-Friends is ignored since OSGi has no apparent equivalent // (could use mandatory export constraints but friends would then // need to use Import-Package to access, rather than Require-Bundle, // which would require knowing which packages are being imported by that dep) if (!myInfo.exportedPackages.isEmpty()) {
StringBuilder b = new StringBuilder(); for (String p : myInfo.exportedPackages) { if (b.length() > 0) {
b.append(", ");
}
b.append(p);
}
osgi.putValue("Export-Package", b.toString());
} for (String attrToCopy : new String[] {"OpenIDE-Module-Layer", "OpenIDE-Module-Install"}) {
String val = netbeans.getValue(attrToCopy); if (val != null) {
osgi.putValue(attrToCopy, val);
}
}
StringBuilder requireBundles = new StringBuilder(); if (!STARTUP_PSEUDO_MODULES.contains(cnb)) { // do not need to import any API, just need it to be started:
requireBundles.append("org.netbeans.core.osgi");
}
Set<String> imports = new TreeSet<>(myInfo.importedPackages);
hideImports(imports, myInfo);
String dependencies = netbeans.getValue("OpenIDE-Module-Module-Dependencies"); if (dependencies != null) { for (String dependency : dependencies.split(" *, *")) {
String depCnb = translateDependency(requireBundles, dependency); if (depCnb == null) { continue;
}
Info imported = infos.get(depCnb); if (imported != null) {
imports.removeAll(imported.exportedPackages);
hideImports(imports, imported);
} else {
log("dependency " + depCnb + " of " + cnb + " not found in batch; imports may not be correct", Project.MSG_WARN);
}
}
} if (requireBundles.length() > 0) {
osgi.putValue("Require-Bundle", requireBundles.toString());
}
StringBuilder staticImports = new StringBuilder();
StringBuilder dynamicImports = new StringBuilder(); for (String pkg : imports) {
StringBuilder b = isOSGiOrJavaPlatform(pkg) ? staticImports : dynamicImports; // JRE-specific dependencies will not be exported by Felix by default. // But Felix can be configured to offer any desired packages from the framework. // Do not use DynamicImport-Package where not really needed, as it can lead to deadlocks in Felix: // ModuleImpl.findClassOrResourceByDelegation -> Felix.acquireGlobalLock if (b.length() > 0) {
b.append(", ");
}
b.append(pkg);
} if (staticImports.length() > 0) {
osgi.putValue("Import-Package", staticImports.toString());
} if (dynamicImports.length() > 0) {
osgi.putValue("DynamicImport-Package", dynamicImports.toString());
} // ignore OpenIDE-Module-Package-Dependencies; rarely used, and bytecode analysis is probably more accurate anyway
String javaDeps = netbeans.getValue("OpenIDE-Module-Java-Dependencies"); if (javaDeps != null) {
Matcher m = Pattern.compile("Java > (1.[6-9])").matcher(javaDeps); // 1.5 is not supported anyway if (m.matches()) {
osgi.putValue("Bundle-RequiredExecutionEnvironment", "JavaSE-" + m.group(1));
}
} for (String tokenAttr : new String[] {"OpenIDE-Module-Provides", "OpenIDE-Module-Needs"}) {
String v = netbeans.getValue(tokenAttr); if (v != null) {
osgi.putValue(tokenAttr, v);
}
}
String v = netbeans.getValue("OpenIDE-Module-Requires"); if (v != null) {
StringBuilder b = null; for (String tok : v.split("[, ]+")) { if (!tok.matches("org.openide.modules.ModuleFormat\\d+")) { if (b == null) {
b = new StringBuilder(tok);
} else {
b.append(", ").append(tok);
}
}
} if (b != null) {
osgi.putValue("OpenIDE-Module-Requires", b.toString());
}
} // autoload, eager status are ignored since OSGi has no apparent equivalent
}
private Map<String,File> findBundledFiles(File module, String cnb) throws Exception {
Map<String,File> result = new HashMap<>(); if (module.getParentFile().getName().matches("modules|core|lib")) {
File cluster = module.getParentFile().getParentFile();
File updateTracking = new File(new File(cluster, "update_tracking"), cnb.replace('.', '-') + ".xml"); if (updateTracking.isFile()) {
Document doc = XMLUtil.parse(new InputSource(updateTracking.toURI().toString()), false, false, null, null);
NodeList nl = doc.getElementsByTagName("file"); for (int i = 0; i < nl.getLength(); i++) {
String path = ((Element) nl.item(i)).getAttribute("name"); if (path.matches("config/(Modules|ModuleAutoDeps)/.+[.]xml|lib/nbexec.*")) { continue;
}
File f = new File(cluster, path); if (f.equals(module)) { continue;
} if (f.isFile()) {
result.put(path, f);
} else {
log("did not find " + f + " specified in " + updateTracking, Project.MSG_WARN);
}
}
} else {
log("did not find expected " + updateTracking, Project.MSG_WARN);
}
} else {
log("JAR " + module + " not found in expected cluster layout", Project.MSG_WARN);
} return result;
}
privatestaticvoid scanClasses(JarFile module, Set<String> importedPackages, Set<String> availablePackages, Task task) throws Exception {
Map<String, byte[]> classfiles = new TreeMap<>();
VerifyClassLinkage.read(module, classfiles, new HashSet<>(Collections.singleton(new File(module.getName()))), task, null); for (Map.Entry<String,byte[]> entry : classfiles.entrySet()) {
String available = entry.getKey(); int idx = available.lastIndexOf('.'); if (idx != -1) {
availablePackages.add(available.substring(0, idx));
} for (String clazz : VerifyClassLinkage.dependencies(entry.getValue())) { if (classfiles.containsKey(clazz)) { // Part of the same module; probably not an external import. continue;
} if (clazz.startsWith("java.")) { // No need to declare as an import. continue;
}
idx = clazz.lastIndexOf('.'); if (idx != -1) {
importedPackages.add(clazz.substring(0, idx));
}
}
}
}
privatevoid processFragment(File fragment) throws Exception {
String cnb = findFragmentHost(fragment);
File bundleFile = new File(destdir, fragment.getName()); if (bundleFile.lastModified() > fragment.lastModified()) {
log("Skipping " + fragment + " since " + bundleFile + " is newer", Project.MSG_VERBOSE); return;
}
log("Processing " + fragment + " into " + bundleFile);
Manifest mf = new Manifest();
Attributes attr = mf.getMainAttributes();
attr.putValue("Manifest-Version", "1.0"); // workaround for JDK bug
attr.putValue("Bundle-ManifestVersion", "2");
attr.putValue("Bundle-SymbolicName", cnb + "-branding");
attr.putValue("Fragment-Host", cnb); try (JarFile jar = new JarFile(fragment);
OutputStream bundle = new FileOutputStream(bundleFile);
ZipOutputStream zos = new JarOutputStream(bundle, mf)) {
Set<String> parents = new HashSet<>();
Enumeration<? extends ZipEntry> entries = jar.entries(); while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String path = entry.getName(); if (path.endsWith("/") || path.equals("META-INF/MANIFEST.MF")) { continue;
} try (InputStream is = jar.getInputStream(entry)) {
writeEntry(zos, path, is, parents);
}
}
zos.finish();
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.