blob: 019a38525c30b22c99b5bf11578898188ff71133 [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.BufferedReader;
import java.util.Map;
import java.util.jar.Manifest;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.DirectoryScanner;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.Collator;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.Path;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
/** Makes an XML file representing update information from NBMs.
*
* @author Jesse Glick
*/
public class MakeUpdateDesc extends MatchingTask {
protected boolean usedMatchingTask = false;
/** Set of NBMs presented as a folder in the Update Center. */
public /*static*/ class Group {
public List<FileSet> filesets = new ArrayList<>();
public String name;
/** Displayed name of the group. */
public void setName (String s) {
name = s;
}
/** Add fileset to the group of NetBeans modules **/
public void addFileSet (FileSet set) {
filesets.add(set);
}
}
/** pointer to another xml entity to include **/
public class Entityinclude {
public String file;
/** Path to the entity file.
* It included as an xml-entity pointer in master .xml file.
*/
public void setFile (String f) {
file = f;
}
}
private List<Entityinclude> entityincludes = new ArrayList<>();
private List<Group> groups = new ArrayList<>();
private List<FileSet> filesets = new ArrayList<>();
private List<String> includeMessageDigests = Arrays.asList("SHA-512");
public String getIncludeMessageDigests() {
return includeMessageDigests.stream().collect(Collectors.joining(" "));
}
public void setIncludeMessageDigests(String includeMessageDigests) {
if(includeMessageDigests == null) {
includeMessageDigests = "";
}
this.includeMessageDigests = Arrays.asList(includeMessageDigests.split(" "));
}
private File desc;
/** Description file to create. */
public void setDesc(File d) {
desc = d;
}
private File descLicense;
/** Description file license to use. */
public void setDescLicense(File d) {
descLicense = d;
}
/** Module group to create **/
public Group createGroup () {
Group g = new Group ();
groups.add (g);
return g;
}
/** External XML entity include **/
public Entityinclude createEntityinclude () {
Entityinclude i = new Entityinclude ();
entityincludes.add (i);
return i;
}
/**
* Adds a set of files (nested fileset attribute).
*/
public void addFileset(FileSet set) {
filesets.add(set);
}
private boolean automaticGrouping;
/**
* Turn on if you want modules added to the root fileset
* to be automatically added to a group based on their display category (if set).
*/
public void setAutomaticgrouping(boolean b) {
automaticGrouping = b;
}
private String dist_base;
/**
* Set distribution base, which will be enforced
*/
public void setDistBase(String dbase) {
dist_base = dbase;
}
private boolean useLicenseUrl;
public void setUseLicenseUrl(boolean useLicenseUrl) {
this.useLicenseUrl = useLicenseUrl;
}
private Path updaterJar;
/** Fileset for platform/modules/ext/updater.jar, to be used in DTD validation. */
public Path createUpdaterJar() {
return updaterJar = new Path(getProject());
}
private String notificationMessage;
private String notificationURL;
public void setNotificationMessage(String message) {
this.notificationMessage = message;
}
public void setNotificationURL(String url) {
this.notificationURL = url;
}
private String contentDescription;
private String contentDescriptionURL;
public void setContentDescription(String message) {
this.contentDescription = message;
}
public void setContentDescriptionURL(String url) {
this.contentDescriptionURL = url;
}
// Similar to org.openide.xml.XMLUtil methods.
private static String xmlEscape(String s) {
int max = s.length();
StringBuffer s2 = new StringBuffer((int)(max * 1.1 + 1));
for (int i = 0; i < max; i++) {
char c = s.charAt(i);
switch (c) {
case '<':
s2.append("&lt;"); //NOI18N
break;
case '>':
s2.append("&gt;"); //NOI18N
break;
case '&':
s2.append("&amp;"); //NOI18N
break;
case '"':
s2.append("&quot;"); //NOI18N
break;
default:
s2.append(c);
break;
}
}
return s2.toString();
}
public @Override void execute () throws BuildException {
Group root = new Group();
for (FileSet fs : filesets) {
root.addFileSet(fs);
}
groups.add(root);
if (desc.exists ()) {
// Simple up-to-date check.
long time = desc.lastModified ();
boolean uptodate = true;
CHECK:
for (Group g : groups) {
for (FileSet n : g.filesets) {
if ( n != null ) {
DirectoryScanner ds = n.getDirectoryScanner(getProject());
String[] files = ds.getIncludedFiles();
File bdir = ds.getBasedir();
for (String file : files) {
File n_file = new File(bdir, file);
if (n_file.lastModified () > time) {
uptodate = false;
break CHECK;
}
}
}
}
}
if (uptodate) return;
}
log ("Creating update description " + desc.getAbsolutePath ());
Map<String,Collection<Module>> modulesByGroup = loadNBMs();
boolean targetClustersDefined = false;
for (Collection<Module> modules : modulesByGroup.values()) {
for (Module m : modules) {
targetClustersDefined |= m.xml.getAttributeNode("targetcluster") != null;
}
}
boolean isPreferredUpdateDefined = false;
for (Collection<Module> modules : modulesByGroup.values()) {
for (Module m : modules) {
isPreferredUpdateDefined |= m.xml.getAttributeNode("preferredupdate") != null;
}
}
boolean use25DTD = false;
for (Collection<Module> modules : modulesByGroup.values()) {
for (Module m : modules) {
Element manifest = ((Element) m.xml.getElementsByTagName("manifest").item(0));
use25DTD |= (m.autoload || m.eager ||
manifest.getAttribute("AutoUpdate-Show-In-Client").length() > 0 ||
manifest.getAttribute("AutoUpdate-Essential-Module").length() > 0);
}
}
// XXX Apparently cannot create a doc with entities using DOM 2.
try {
desc.delete();
OutputStream os = new FileOutputStream(desc);
try {
PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8")); //NOI18N
pw.println ("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"); //NOI18N
pw.println ();
if (descLicense != null) {
pw.println(new String(Files.readAllBytes(descLicense.toPath()), "UTF-8"));
}
DateFormat format = new SimpleDateFormat("ss/mm/HH/dd/MM/yyyy"); //NOI18N
format.setTimeZone(TimeZone.getTimeZone("GMT")); //NOI18N
String date = format.format(new Date());
if ( entityincludes.size() > 0 ) {
// prepare .ent file
String ent_name = desc.getAbsolutePath();
int xml_idx = ent_name.indexOf(".xml"); //NOI18N
if (xml_idx != -1) {
ent_name = ent_name.substring (0, xml_idx) + ".ent"; //NOI18N
} else {
ent_name = ent_name + ".ent"; //NOI18N
}
File desc_ent = new File(ent_name);
desc_ent.delete();
if (includeMessageDigests != null && (! includeMessageDigests.isEmpty())) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.8//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_8.dtd\" [");
} else if (isPreferredUpdateDefined || (contentDescription != null && ! contentDescription.isEmpty())) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.7//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_7.dtd\" [");
} else if (useLicenseUrl) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.6//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_6.dtd\" [");
} else if (use25DTD) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.5//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_5.dtd\" [");
} else if (targetClustersDefined) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.4//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_4.dtd\" [");
} else {
// #74866: no need for targetcluster, so keep compat w/ 5.0 AU.
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.3//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_3.dtd\" [");
}
// Would be better to follow order of groups and includes
pw.println (" <!ENTITY entity SYSTEM \"" + xmlEscape(desc_ent.getName()) + "\">"); //NOI18N
int inc_num=0;
for (int i=0; i<entityincludes.size(); i++) {
Entityinclude ei = entityincludes.get(i);
pw.println (" <!ENTITY include" + i + " SYSTEM \"" + xmlEscape(ei.file) + "\">"); //NOI18N
}
pw.println ("]>"); //NOI18N
pw.println ();
pw.println ("<module_updates timestamp=\"" + xmlEscape(date) + "\">"); //NOI18N
pw.println (" &entity;"); //NOI18N
for (int i=0; i<entityincludes.size(); i++) {
pw.println (" &include" + i + ";"); //NOI18N
}
pw.println ("</module_updates>"); //NOI18N
pw.println ();
pw.flush ();
pw.close ();
os = new FileOutputStream(desc_ent);
pw = new PrintWriter(new OutputStreamWriter(os, "UTF-8")); //NOI18N
pw.println ("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"); //NOI18N
pw.println ("<!-- external entity include " + date + " -->");
pw.println ();
} else {
if (includeMessageDigests != null && (! includeMessageDigests.isEmpty())) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.8//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_8.dtd\">");
} else if (isPreferredUpdateDefined || (contentDescription != null && ! contentDescription.isEmpty())) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.7//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_7.dtd\">");
} else if (useLicenseUrl) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.6//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_6.dtd\">");
} else if (use25DTD) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.5//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_5.dtd\">");
} else if (targetClustersDefined) {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.4//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_4.dtd\">");
} else {
pw.println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.3//EN\" \"https://netbeans.apache.org/dtds/autoupdate-catalog-2_3.dtd\">");
}
pw.println ("<module_updates timestamp=\"" + date + "\">"); //NOI18N
pw.println ();
}
writeNotification(pw);
pw.println ();
writeContentDescription(pw);
pw.println ();
Map<String,Element> licenses = new HashMap<>();
String prefix = null;
if (dist_base != null) {
// fix/enforce distribution URL base
if (dist_base.equals(".")) {
prefix = "";
} else {
prefix = dist_base + "/";
}
}
final File licensesDir = new File(desc.getParentFile(), "licenses");
if (useLicenseUrl) {
if (licensesDir.exists()) {
File [] licenseFiles = licensesDir.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.getName().endsWith(".license");
}
});
for (File f : licenseFiles) {
f.delete();
}
} else {
licensesDir.mkdir();
}
}
for (Map.Entry<String,Collection<Module>> entry : modulesByGroup.entrySet()) {
String groupName = entry.getKey();
// Don't indent; embedded descriptions would get indented otherwise.
log("Creating group \"" + groupName + "\"");
if (groupName != null) {
pw.println("<module_group name=\"" + xmlEscape(groupName) + "\">");
pw.println();
}
for (Module m : entry.getValue()) {
Element module = m.xml;
if (module.getAttribute("downloadsize").equals("0")) {
module.setAttribute("downloadsize", Long.toString(m.nbm.length() + m.externalDownloadSize));
}
Element manifest = (Element) module.getElementsByTagName("manifest").item(0);
String name = manifest.getAttribute("OpenIDE-Module-Name");
if (name.length() > 0) {
log(" Adding module " + name + " (" + m.nbm.getAbsolutePath() + ")");
}
if(prefix!=null) {
module.setAttribute("distribution", prefix + m.relativePath);
}
NodeList licenseList = module.getElementsByTagName("license");
if (licenseList.getLength() > 0) {
Element license = (Element) licenseList.item(0);
if (useLicenseUrl) {
String relativePath = "licenses/" + license.getAttribute("name") + ".license";
String path = relativePath;
if (prefix != null) {
path = prefix + relativePath;
}
license.setAttribute("url", path);
String licenseText = license.getTextContent();
license.setTextContent("");
try (FileOutputStream fos = new FileOutputStream(new File(desc.getParentFile(), relativePath))) {
fos.write(licenseText.getBytes("UTF-8"));
}
}
// XXX ideally would compare the license texts to make sure they actually match up
licenses.put(license.getAttribute("name"), license);
module.removeChild(license);
}
if (m.autoload) {
module.setAttribute("autoload", "true");
}
if (m.eager) {
module.setAttribute("eager", "true");
}
pw.flush();
XMLUtil.write(module, os);
pw.println();
}
if (groupName != null) {
pw.println("</module_group>");
pw.println();
}
}
pw.flush();
for (Element license : licenses.values()) {
XMLUtil.write(license, os);
}
if ( entityincludes.size() <= 0 ) {
pw.println ("</module_updates>"); //NOI18N
pw.println ();
}
pw.flush ();
pw.close ();
} finally {
os.flush ();
os.close ();
}
} catch (IOException ioe) {
desc.delete ();
throw new BuildException("Cannot create update description", ioe, getLocation());
}
if (updaterJar != null && updaterJar.size() > 0) {
try {
MakeNBM.validateAgainstAUDTDs(new InputSource(desc.toURI().toString()), updaterJar, this);
} catch (Exception x) {
desc.delete();
throw new BuildException("Could not validate " + desc + " after writing: " + x, x, getLocation());
}
} else {
log("No updater.jar specified, cannot validate " + desc + " against DTD", Project.MSG_WARN);
}
}
private static class Module {
public Module() {}
public Element xml;
public File nbm;
public String relativePath;
public boolean autoload, eager;
public long externalDownloadSize;
}
private void writeNotification(PrintWriter pw) {
// write notification message/url if defined
if (notificationMessage == null) {
notificationMessage = "";
}
if (notificationURL == null) {
notificationURL = "";
}
if (notificationMessage.length() > 0 || notificationURL.length() > 0) {
if (notificationMessage.length() == 0) {
pw.println("<notification url=\"" + xmlEscape(notificationURL.toString()) + "\"/>");
} else if (notificationURL.length() == 0) {
pw.println("<notification>" + xmlEscape(notificationMessage) + "</notification>");
} else {
pw.println(
"<notification url=\"" + xmlEscape(notificationURL.toString()) + "\">" +
xmlEscape(notificationMessage) +
"</notification>");
}
pw.println();
}
}
private void writeContentDescription(PrintWriter pw) {
// write content_description message/url if defined
if (contentDescription == null) {
contentDescription = "";
}
if (contentDescriptionURL == null) {
contentDescriptionURL = "";
}
if (contentDescription.length() > 0 || contentDescriptionURL.length() > 0) {
if (contentDescription.length() == 0) {
pw.println("<content_description url=\"" + xmlEscape(contentDescriptionURL.toString()) + "\"/>");
} else if (contentDescriptionURL.length() == 0) {
pw.println("<content_description>" + xmlEscape(contentDescription) + "</content_description>");
} else {
pw.println(
"<content_description url=\"" + xmlEscape(contentDescriptionURL.toString()) + "\">" +
xmlEscape(contentDescription) +
"</content_description>");
}
pw.println();
}
}
private Map<String,Collection<Module>> loadNBMs() throws BuildException {
final Collator COLL = Collator.getInstance(/* XXX any particular locale? */);
// like COLL but handles nulls ~ ungrouped modules (sorted to top):
Comparator<String> groupNameComparator = new Comparator<String>() {
public int compare(String gn1, String gn2) {
return gn1 != null ?
(gn2 != null ? COLL.compare(gn1, gn2) : 1) :
(gn2 != null ? -1 : 0);
}
};
Map<String,Collection<Module>> r = automaticGrouping ?
// generally will be creating groups on the fly, so sort them:
new TreeMap<>(groupNameComparator) :
// preserve explicit order of <group>s:
new LinkedHashMap<>();
// sort modules by display name (where available):
Comparator<Module> moduleDisplayNameComparator = new Comparator<Module>() {
public int compare(Module m1, Module m2) {
int res = COLL.compare(getName(m1), getName(m2));
return res != 0 ? res : System.identityHashCode(m1) - System.identityHashCode(m2);
}
String getName(Module m) {
Element mani = (Element) m.xml.getElementsByTagName("manifest").item(0);
String displayName = mani.getAttribute("OpenIDE-Module-Name");
if (displayName.length() > 0) {
return displayName;
} else {
return mani.getAttribute("OpenIDE-Module");
}
}
};
for (Group g : groups) {
Collection<Module> modules = r.get(g.name);
if (modules == null) {
modules = new TreeSet<>(moduleDisplayNameComparator);
r.put(g.name, modules);
}
for (FileSet fs : g.filesets) {
DirectoryScanner ds = fs.getDirectoryScanner(getProject());
for (String file : ds.getIncludedFiles()) {
File n_file = new File(fs.getDir(getProject()), file);
try {
try (JarFile jar = new JarFile(n_file)) {
Module m = new Module();
m.nbm = n_file;
m.relativePath = file.replace(File.separatorChar, '/');
Manifest jarmani = jar.getManifest();
if (jarmani != null && jarmani.getMainAttributes().getValue("Bundle-SymbolicName") != null) {
// #181025: treat OSGi bundles specially.
m.xml = fakeOSGiInfoXml(jar, n_file);
modules.add(m);
continue;
}
ZipEntry entry = jar.getEntry("Info/info.xml");
if (entry == null) {
throw new BuildException("NBM " + n_file + " was malformed: no Info/info.xml", getLocation());
}
InputStream is = jar.getInputStream(entry);
try {
m.xml = XMLUtil.parse(new InputSource(is), false, false, XMLUtil.rethrowHandler(), XMLUtil.nullResolver()).getDocumentElement();
} finally {
is.close();
}
Collection<Module> moduleCollection = modules;
Element manifest = ((Element) m.xml.getElementsByTagName("manifest").item(0));
if (automaticGrouping && g.name == null) {
// insert modules with no explicit grouping into group acc. to manifest:
String categ = manifest.getAttribute("OpenIDE-Module-Display-Category");
if (categ.length() > 0) {
moduleCollection = r.get(categ);
if (moduleCollection == null) {
moduleCollection = new TreeSet<>(moduleDisplayNameComparator);
r.put(categ, moduleCollection);
}
}
}
boolean old = false; // #110661
String destDir = getProject().getProperty("netbeans.dest.dir");
if (destDir != null) {
for (File cluster : getProject().resolveFile(destDir).listFiles()) {
if (new File(cluster, "modules/org-netbeans-modules-autoupdate.jar").isFile()) {
old = true;
break;
}
}
}
if (!old) {
String cnb = manifest.getAttribute("OpenIDE-Module").replaceFirst("/\\d+$", "");
entry = jar.getEntry("netbeans/config/Modules/" + cnb.replace('.', '-') + ".xml");
if (entry != null) {
is = jar.getInputStream(entry);
try {
NodeList nl = XMLUtil.parse(new InputSource(is), false, false, XMLUtil.rethrowHandler(), XMLUtil.nullResolver()).getElementsByTagName("param");
for (int i = 0; i < nl.getLength(); i++) {
String name = ((Element) nl.item(i)).getAttribute("name");
String value = ((Text) nl.item(i).getFirstChild()).getData();
if (name.equals("autoload") && value.equals("true")) {
m.autoload = true;
}
if (name.equals("eager") && value.equals("true")) {
m.eager = true;
}
}
} finally {
is.close();
}
}
}
Enumeration<JarEntry> en = jar.entries();
while (en.hasMoreElements()) {
JarEntry e = en.nextElement();
if (e.getName().endsWith(".external")) {
try (InputStream eStream = jar.getInputStream(e)) {
m.externalDownloadSize += externalSize(eStream);
}
}
}
Map<String,String> messageDigests = createMessageDigests(n_file);
for(Entry<String,String> messageDigest: messageDigests.entrySet()) {
Element messageDigestElement = m.xml.getOwnerDocument().createElement("message_digest");
messageDigestElement.setAttribute("algorithm", messageDigest.getKey());
messageDigestElement.setAttribute("value", messageDigest.getValue());
m.xml.appendChild(messageDigestElement);
}
moduleCollection.add(m);
}
} catch (BuildException x) {
throw x;
} catch (Exception e) {
throw new BuildException("Cannot process " + n_file + ": " + e, e, getLocation());
}
}
}
}
return r;
}
private long externalSize(InputStream is) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
for (;;) {
String line = br.readLine();
if (line == null) {
break;
}
if (line.startsWith("SIZE:")) {
return Long.parseLong(line.substring(5).trim());
}
}
return 0;
}
/**
* Create the equivalent of {@code Info/info.xml} for an OSGi bundle.
* @param jar a bundle
* @return a {@code <module ...><manifest .../></module>} valid according to
* <a href="https://netbeans.apache.org/dtds/autoupdate-info-2_5.dtd">DTD</a>
*/
private static Element fakeOSGiInfoXml(JarFile jar, File whereFrom) throws IOException {
return fakeOSGiInfoXml(jar, whereFrom, XMLUtil.createDocument("module"));
}
//TODO: javadoc
public static Element fakeOSGiInfoXml(JarFile jar, File whereFrom, Document doc) throws IOException {
Attributes attr = jar.getManifest().getMainAttributes();
Properties localized = new Properties();
String bundleLocalization = attr.getValue("Bundle-Localization");
if (bundleLocalization != null) {
try (InputStream is = jar.getInputStream(jar.getEntry(bundleLocalization + ".properties"))) {
localized.load(is);
}
}
return fakeOSGiInfoXml(attr, localized, whereFrom, doc);
}
static Element fakeOSGiInfoXml(Attributes attr, Properties localized, File whereFrom) { //tests
return fakeOSGiInfoXml(attr, localized, whereFrom, XMLUtil.createDocument("module"));
}
private static Element fakeOSGiInfoXml(Attributes attr, Properties localized, File whereFrom, Document doc) {
Element module = doc.getDocumentElement();
String cnb = JarWithModuleAttributes.extractCodeName(attr);
module.setAttribute("codenamebase", cnb);
module.setAttribute("distribution", ""); // seems to be ignored anyway
module.setAttribute("downloadsize", "0"); // recalculated anyway
module.setAttribute("targetcluster", whereFrom.getParentFile().getName()); // #207075 comment #3
Element manifest = doc.createElement("manifest");
module.appendChild(manifest);
manifest.setAttribute("AutoUpdate-Show-In-Client", "false");
manifest.setAttribute("OpenIDE-Module", cnb);
String bundleName = loc(localized, attr, "Bundle-Name");
manifest.setAttribute("OpenIDE-Module-Name", bundleName != null ? bundleName : cnb);
String bundleVersion = attr.getValue("Bundle-Version");
manifest.setAttribute("OpenIDE-Module-Specification-Version",
bundleVersion != null ? bundleVersion.replaceFirst("^(\\d+([.]\\d+([.]\\d+)?)?)([.].+)?$", "$1") : "0");
String requireBundle = attr.getValue("Require-Bundle");
if (requireBundle != null) {
StringBuilder b = new StringBuilder();
boolean needsNetbinox = false;
// http://stackoverflow.com/questions/1757065/java-splitting-a-comma-separated-string-but-ignoring-commas-in-quotes
for (String dep : requireBundle.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")) {
Matcher m = Pattern.compile("([^;]+)(.*)").matcher(dep);
if (!m.matches()) {
throw new BuildException("Could not parse dependency: " + dep + " in " + whereFrom);
}
String requiredBundleName = m.group(1); // dep CNB
if(requiredBundleName.trim().equals("org.eclipse.osgi")) {
needsNetbinox = true;
continue;
}
Matcher m2 = Pattern.compile(";([^:=]+):?=\"?([^;\"]+)\"?").matcher(m.group(2));
boolean isOptional = false;
while(m2.find()) {
if(m2.group(1).equals("resolution") && m2.group(2).equals("optional")) {
isOptional = true;
break;
}
}
if(isOptional) {
continue;
}
m2.reset();
StringBuilder depSB = new StringBuilder();
depSB.append(requiredBundleName.replace('-', '_')); // dep CNB
while (m2.find()) {
if (!m2.group(1).equals("bundle-version")) {
continue;
}
String val = m2.group(2);
if (val.matches("[0-9]+([.][0-9]+)*")) {
// non-range dep occasionally used in OSGi; no exact equivalent in NB
depSB.append(" > ").append(val);
continue;
}
Matcher m3 = Pattern.compile("\\[([0-9]+)((?:[.][0-9]+)*),([0-9.]+)\\)").matcher(val);
if (!m3.matches()) {
throw new BuildException("Could not parse version range: " + val + " in " + whereFrom);
}
int major = Integer.parseInt(m3.group(1));
String rest = m3.group(2);
try {
int max = Integer.parseInt(m3.group(3));
if (major > 99) {
depSB.append('/').append(major / 100);
if (max > major + 100) {
depSB.append('-').append(max / 100 - 1);
}
} else if (max > 100) {
depSB.append("/0-").append(max / 100 - 1);
}
} catch (NumberFormatException x) {
// never mind end boundary, does not match NB conventions
}
depSB.append(" > ").append(major % 100).append(rest);
}
if (b.indexOf(depSB.toString()) == -1) {
if (b.length() > 0) {
b.append(", ");
}
b.append(depSB);
}
}
if(b.length() > 0) {
manifest.setAttribute("OpenIDE-Module-Module-Dependencies", b.toString());
}
if(needsNetbinox) {
manifest.setAttribute("OpenIDE-Module-Needs", "org.netbeans.Netbinox");
}
}
String provides = computeExported(attr).toString();
if (! provides.isEmpty()) {
manifest.setAttribute("OpenIDE-Module-Provides", provides);
}
String recommends = computeImported(attr).toString();
if (! recommends.isEmpty()) {
manifest.setAttribute("OpenIDE-Module-Recommends", recommends);
}
String bundleCategory = loc(localized, attr, "Bundle-Category");
if (bundleCategory != null) {
manifest.setAttribute("OpenIDE-Module-Display-Category", bundleCategory);
}
String bundleDescription = loc(localized, attr, "Bundle-Description");
if (bundleDescription != null) {
manifest.setAttribute("OpenIDE-Module-Short-Description", bundleDescription);
}
// XXX anything else need to be set?
return module;
}
private static String loc(Properties localized, Attributes attr, String key) {
String val = attr.getValue(key);
if (val == null) {
return null;
} else if (val.startsWith("%")) {
return localized.getProperty(val.substring(1));
} else {
return val;
}
}
private static StringTokenizer createTokenizer(String osgiDep) {
for (;;) {
int first = osgiDep.indexOf('"');
if (first == -1) {
break;
}
int second = osgiDep.indexOf('"', first + 1);
if (second == -1) {
break;
}
osgiDep = osgiDep.substring(0, first - 1) + osgiDep.substring(second + 1);
}
return new StringTokenizer(osgiDep, ",");
}
private static String beforeSemicolon(StringTokenizer tok) {
String dep = tok.nextToken().trim();
int semicolon = dep.indexOf(';');
if (semicolon >= 0) {
dep = dep.substring(0, semicolon);
}
return dep.replace('-', '_');
}
private static StringBuilder computeExported(Attributes attr) {
StringBuilder sb = new StringBuilder();
String pkgs = attr.getValue("Export-Package"); // NOI18N
if (pkgs == null) {
return sb;
}
StringTokenizer tok = createTokenizer(pkgs); // NOI18N
String token;
while (tok.hasMoreElements()) {
token = beforeSemicolon(tok);
if (sb.indexOf(token) == -1) {
sb.append(sb.length() == 0 ? "" : ", ").append(token);
}
}
return sb;
}
private static StringBuilder computeImported(Attributes attr) {
StringBuilder sb = new StringBuilder();
String pkgs = attr.getValue("Import-Package"); // NOI18N
if (pkgs != null) {
StringTokenizer tok = createTokenizer(pkgs); // NOI18N
String token;
while (tok.hasMoreElements()) {
token = beforeSemicolon(tok);
if (sb.indexOf(token) == -1) {
sb.append(sb.length() == 0 ? "" : ", ").append(token);
}
}
}
String recomm = attr.getValue("Require-Bundle"); // NOI18N
if (recomm != null) {
StringTokenizer tok = createTokenizer(recomm); // NOI18N
String token;
while (tok.hasMoreElements()) {
token = beforeSemicolon(tok);
if (sb.indexOf(token) == -1) {
sb.append(sb.length() == 0 ? "" : ", ").append("cnb.").append(token);
}
}
}
return sb;
}
private Map<String,String> createMessageDigests(File targetFile) {
if(includeMessageDigests.isEmpty()) {
return Collections.<String,String>emptyMap();
}
Map<String,MessageDigest> digests = new HashMap<>(includeMessageDigests.size());
for(String messageDigest: includeMessageDigests) {
try {
digests.put(messageDigest, MessageDigest.getInstance(messageDigest));
} catch (NoSuchAlgorithmException ex) {
throw new BuildException("Failed to load requested messageDigest: " + messageDigest, ex);
}
}
try(FileInputStream fis = new FileInputStream(targetFile)) {
byte[] buffer = new byte[102400];
int read;
while((read = fis.read(buffer)) >= 0) {
for(MessageDigest md: digests.values()) {
md.update(buffer, 0, read);
}
}
} catch (IOException ex) {
Logger.getLogger(MakeUpdateDesc.class.getName()).log(Level.SEVERE, null, ex);
}
Map<String,String> result = new HashMap<>();
for(Entry<String,MessageDigest> md: digests.entrySet()) {
result.put(md.getKey(), hexEncode(md.getValue().digest()));
}
return result;
}
private static String hexEncode(byte[] input) {
StringBuilder sb = new StringBuilder(input.length * 2);
for(byte b: input) {
sb.append(Character.forDigit((b & 0xF0) >> 4, 16));
sb.append(Character.forDigit((b & 0x0F), 16));
}
return sb.toString();
}
private static byte[] hexDecode(String input) {
int length = input.length() / 2;
byte[] result = new byte[length];
for(int i = 0; i < length; i++) {
int b = Character.digit(input.charAt(i * 2), 16) << 4;
b |= Character.digit(input.charAt(i * 2 + 1), 16);
result[i] = (byte) (b & 0xFF);
}
return result;
}
}