001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.nbbuild;
043:
044: import java.io.ByteArrayInputStream;
045: import java.util.HashMap;
046: import java.util.LinkedHashMap;
047: import java.util.Map;
048: import org.apache.tools.ant.types.FileSet;
049: import org.apache.tools.ant.BuildException;
050: import org.apache.tools.ant.taskdefs.MatchingTask;
051: import org.apache.tools.ant.DirectoryScanner;
052: import java.io.File;
053: import java.io.FileOutputStream;
054: import java.io.IOException;
055: import java.io.InputStream;
056: import java.io.OutputStream;
057: import java.io.OutputStreamWriter;
058: import java.io.PrintWriter;
059: import java.text.Collator;
060: import java.text.DateFormat;
061: import java.text.SimpleDateFormat;
062: import java.util.ArrayList;
063: import java.util.Collection;
064: import java.util.Comparator;
065: import java.util.Date;
066: import java.util.HashSet;
067: import java.util.List;
068: import java.util.Set;
069: import java.util.TimeZone;
070: import java.util.TreeMap;
071: import java.util.TreeSet;
072: import java.util.zip.ZipEntry;
073: import java.util.zip.ZipFile;
074: import org.w3c.dom.Element;
075: import org.w3c.dom.NodeList;
076: import org.w3c.dom.Text;
077: import org.xml.sax.EntityResolver;
078: import org.xml.sax.InputSource;
079: import org.xml.sax.SAXException;
080:
081: /** Makes an XML file representing update information from NBMs.
082: *
083: * @author Jesse Glick
084: */
085: public class MakeUpdateDesc extends MatchingTask {
086:
087: protected boolean usedMatchingTask = false;
088:
089: /** Set of NBMs presented as a folder in the Update Center. */
090: public/*static*/class Group {
091: public List<FileSet> filesets = new ArrayList<FileSet>();
092: public String name;
093:
094: /** Displayed name of the group. */
095: public void setName(String s) {
096: name = s;
097: }
098:
099: /** Add fileset to the group of NetBeans modules **/
100: public void addFileSet(FileSet set) {
101: filesets.add(set);
102: }
103: }
104:
105: /** pointer to another xml entity to include **/
106: public class Entityinclude {
107: public String file;
108:
109: /** Path to the entity file.
110: * It included as an xml-entity pointer in master .xml file.
111: */
112: public void setFile(String f) {
113: file = f;
114: }
115: }
116:
117: private List<Entityinclude> entityincludes = new ArrayList<Entityinclude>();
118: private List<Group> groups = new ArrayList<Group>();
119: private List<FileSet> filesets = new ArrayList<FileSet>();
120:
121: private File desc;
122:
123: /** Description file to create. */
124: public void setDesc(File d) {
125: desc = d;
126: }
127:
128: /** Module group to create **/
129: public Group createGroup() {
130: Group g = new Group();
131: groups.add(g);
132: return g;
133: }
134:
135: /** External XML entity include **/
136: public Entityinclude createEntityinclude() {
137: Entityinclude i = new Entityinclude();
138: entityincludes.add(i);
139: return i;
140: }
141:
142: /**
143: * Adds a set of files (nested fileset attribute).
144: */
145: public void addFileset(FileSet set) {
146: filesets.add(set);
147: }
148:
149: private boolean automaticGrouping;
150:
151: /**
152: * Turn on if you want modules added to the root fileset
153: * to be automatically added to a group based on their display category (if set).
154: */
155: public void setAutomaticgrouping(boolean b) {
156: automaticGrouping = b;
157: }
158:
159: private String dist_base;
160:
161: /**
162: * Set distribution base, which will be enforced
163: */
164: public void setDistBase(String dbase) {
165: dist_base = dbase;
166: }
167:
168: // Similar to org.openide.xml.XMLUtil methods.
169: private static String xmlEscape(String s) {
170: int max = s.length();
171: StringBuffer s2 = new StringBuffer((int) (max * 1.1 + 1));
172: for (int i = 0; i < max; i++) {
173: char c = s.charAt(i);
174: switch (c) {
175: case '<':
176: s2.append("<"); //NOI18N
177: break;
178: case '>':
179: s2.append(">"); //NOI18N
180: break;
181: case '&':
182: s2.append("&"); //NOI18N
183: break;
184: case '"':
185: s2.append("""); //NOI18N
186: break;
187: default:
188: s2.append(c);
189: break;
190: }
191: }
192: return s2.toString();
193: }
194:
195: public @Override
196: void execute() throws BuildException {
197: Group root = new Group();
198: for (FileSet fs : filesets) {
199: root.addFileSet(fs);
200: }
201: groups.add(root);
202: if (desc.exists()) {
203: // Simple up-to-date check.
204: long time = desc.lastModified();
205: boolean uptodate = true;
206:
207: CHECK: for (Group g : groups) {
208: for (FileSet n : g.filesets) {
209: if (n != null) {
210: DirectoryScanner ds = n
211: .getDirectoryScanner(getProject());
212: String[] files = ds.getIncludedFiles();
213: File bdir = ds.getBasedir();
214: for (String file : files) {
215: File n_file = new File(bdir, file);
216: if (n_file.lastModified() > time) {
217: uptodate = false;
218: break CHECK;
219: }
220: }
221: }
222: }
223: }
224: if (uptodate)
225: return;
226: }
227: log("Creating update description " + desc.getAbsolutePath());
228:
229: Map<String, Collection<Module>> modulesByGroup = loadNBMs();
230: boolean targetClustersDefined = false;
231: for (Collection<Module> modules : modulesByGroup.values()) {
232: for (Module m : modules) {
233: targetClustersDefined |= m.xml
234: .getAttributeNode("targetcluster") != null;
235: }
236: }
237: boolean use25DTD = false;
238: for (Collection<Module> modules : modulesByGroup.values()) {
239: for (Module m : modules) {
240: Element manifest = ((Element) m.xml
241: .getElementsByTagName("manifest").item(0));
242: use25DTD |= (m.autoload
243: || m.eager
244: || manifest.getAttribute(
245: "AutoUpdate-Show-In-Client").length() > 0 || manifest
246: .getAttribute("AutoUpdate-Essential-Module")
247: .length() > 0);
248: }
249: }
250:
251: // XXX Apparently cannot create a doc with entities using DOM 2.
252: try {
253: desc.delete();
254: OutputStream os = new FileOutputStream(desc);
255: try {
256:
257: PrintWriter pw = new PrintWriter(
258: new OutputStreamWriter(os, "UTF-8")); //NOI18N
259: pw
260: .println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"); //NOI18N
261: pw.println();
262: DateFormat format = new SimpleDateFormat(
263: "ss/mm/HH/dd/MM/yyyy"); //NOI18N
264: format.setTimeZone(TimeZone.getTimeZone("GMT")); //NOI18N
265: String date = format.format(new Date());
266:
267: if (entityincludes.size() > 0) {
268: // prepare .ent file
269: String ent_name = desc.getAbsolutePath();
270: int xml_idx = ent_name.indexOf(".xml"); //NOI18N
271: if (xml_idx != -1) {
272: ent_name = ent_name.substring(0, xml_idx)
273: + ".ent"; //NOI18N
274: } else {
275: ent_name = ent_name + ".ent"; //NOI18N
276: }
277: File desc_ent = new File(ent_name);
278: desc_ent.delete();
279: if (use25DTD) {
280: pw
281: .println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.5//EN\" \"http://www.netbeans.org/dtds/autoupdate-catalog-2_5.dtd\" [");
282: } else if (targetClustersDefined) {
283: pw
284: .println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.4//EN\" \"http://www.netbeans.org/dtds/autoupdate-catalog-2_4.dtd\" [");
285: } else {
286: // #74866: no need for targetcluster, so keep compat w/ 5.0 AU.
287: pw
288: .println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.3//EN\" \"http://www.netbeans.org/dtds/autoupdate-catalog-2_3.dtd\" [");
289: }
290: // Would be better to follow order of groups and includes
291: pw.println(" <!ENTITY entity SYSTEM \""
292: + xmlEscape(desc_ent.getName()) + "\">"); //NOI18N
293: int inc_num = 0;
294: for (int i = 0; i < entityincludes.size(); i++) {
295: Entityinclude ei = entityincludes.get(i);
296: pw.println(" <!ENTITY include" + i
297: + " SYSTEM \"" + xmlEscape(ei.file)
298: + "\">"); //NOI18N
299: }
300: pw.println("]>"); //NOI18N
301: pw.println();
302: pw.println("<module_updates timestamp=\""
303: + xmlEscape(date) + "\">"); //NOI18N
304: pw.println(" &entity;"); //NOI18N
305: for (int i = 0; i < entityincludes.size(); i++) {
306: pw.println(" &include" + i + ";"); //NOI18N
307: }
308: pw.println("</module_updates>"); //NOI18N
309: pw.println();
310: pw.flush();
311: pw.close();
312:
313: os = new FileOutputStream(desc_ent);
314: pw = new PrintWriter(new OutputStreamWriter(os,
315: "UTF-8")); //NOI18N
316: pw
317: .println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"); //NOI18N
318: pw.println("<!-- external entity include " + date
319: + " -->");
320: pw.println();
321:
322: } else {
323: if (use25DTD) {
324: pw
325: .println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.5//EN\" \"http://www.netbeans.org/dtds/autoupdate-catalog-2_5.dtd\">");
326: } else if (targetClustersDefined) {
327: pw
328: .println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.4//EN\" \"http://www.netbeans.org/dtds/autoupdate-catalog-2_4.dtd\">");
329: } else {
330: pw
331: .println("<!DOCTYPE module_updates PUBLIC \"-//NetBeans//DTD Autoupdate Catalog 2.3//EN\" \"http://www.netbeans.org/dtds/autoupdate-catalog-2_3.dtd\">");
332: }
333: pw.println("<module_updates timestamp=\"" + date
334: + "\">"); //NOI18N
335: pw.println();
336: }
337:
338: pw.println();
339: Map<String, Element> licenses = new HashMap<String, Element>();
340: Set<String> licenseNames = new HashSet<String>();
341:
342: for (Map.Entry<String, Collection<Module>> entry : modulesByGroup
343: .entrySet()) {
344: String groupName = entry.getKey();
345: // Don't indent; embedded descriptions would get indented otherwise.
346: log("Creating group \"" + groupName + "\"");
347: if (groupName != null) {
348: pw.println("<module_group name=\""
349: + xmlEscape(groupName) + "\">");
350: pw.println();
351: }
352: for (Module m : entry.getValue()) {
353: Element module = m.xml;
354: if (module.getAttribute("downloadsize").equals(
355: "0")) {
356: module.setAttribute("downloadsize", Long
357: .toString(m.nbm.length()));
358: }
359: Element manifest = (Element) module
360: .getElementsByTagName("manifest").item(
361: 0);
362: String name = manifest
363: .getAttribute("OpenIDE-Module-Name");
364: if (name.length() > 0) {
365: log(" Adding module " + name + " ("
366: + m.nbm.getAbsolutePath() + ")");
367: }
368: if (dist_base != null) {
369: // fix/enforce distribution URL base
370: String prefix;
371: if (dist_base.equals(".")) {
372: prefix = "";
373: } else {
374: prefix = dist_base + "/";
375: }
376: module.setAttribute("distribution", prefix
377: + m.relativePath);
378: }
379: NodeList licenseList = module
380: .getElementsByTagName("license");
381: if (licenseList.getLength() > 0) {
382: Element license = (Element) licenseList
383: .item(0);
384: // XXX ideally would compare the license texts to make sure they actually match up
385: licenses.put(license.getAttribute("name"),
386: license);
387: module.removeChild(license);
388: }
389: if (m.autoload) {
390: module.setAttribute("autoload", "true");
391: }
392: if (m.eager) {
393: module.setAttribute("eager", "true");
394: }
395: pw.flush();
396: XMLUtil.write(module, os);
397: pw.println();
398: }
399: if (groupName != null) {
400: pw.println("</module_group>");
401: pw.println();
402: }
403: }
404: pw.flush();
405: for (Element license : licenses.values()) {
406: XMLUtil.write(license, os);
407: }
408: if (entityincludes.size() <= 0) {
409: pw.println("</module_updates>"); //NOI18N
410: pw.println();
411: }
412: pw.flush();
413: pw.close();
414: } finally {
415: os.flush();
416: os.close();
417: }
418: } catch (IOException ioe) {
419: desc.delete();
420: throw new BuildException(
421: "Cannot create update description", ioe,
422: getLocation());
423: }
424: }
425:
426: private static class Module {
427: public Module() {
428: }
429:
430: public Element xml;
431: public File nbm;
432: public String relativePath;
433: public boolean autoload, eager;
434: }
435:
436: private Map<String, Collection<Module>> loadNBMs()
437: throws BuildException {
438: final Collator COLL = Collator
439: .getInstance(/* XXX any particular locale? */);
440: // like COLL but handles nulls ~ ungrouped modules (sorted to top):
441: Comparator<String> groupNameComparator = new Comparator<String>() {
442: public int compare(String gn1, String gn2) {
443: return gn1 != null ? (gn2 != null ? COLL.compare(gn1,
444: gn2) : 1) : (gn2 != null ? -1 : 0);
445: }
446: };
447: Map<String, Collection<Module>> r = automaticGrouping ?
448: // generally will be creating groups on the fly, so sort them:
449: new TreeMap<String, Collection<Module>>(groupNameComparator)
450: :
451: // preserve explicit order of <group>s:
452: new LinkedHashMap<String, Collection<Module>>();
453: // sort modules by display name (where available):
454: Comparator<Module> moduleDisplayNameComparator = new Comparator<Module>() {
455: public int compare(Module m1, Module m2) {
456: int res = COLL.compare(getName(m1), getName(m2));
457: return res != 0 ? res : System.identityHashCode(m1)
458: - System.identityHashCode(m2);
459: }
460:
461: String getName(Module m) {
462: Element mani = (Element) m.xml.getElementsByTagName(
463: "manifest").item(0);
464: String displayName = mani
465: .getAttribute("OpenIDE-Module-Name");
466: if (displayName.length() > 0) {
467: return displayName;
468: } else {
469: return mani.getAttribute("OpenIDE-Module");
470: }
471: }
472: };
473: for (Group g : groups) {
474: Collection<Module> modules = r.get(g.name);
475: if (modules == null) {
476: modules = new TreeSet<Module>(
477: moduleDisplayNameComparator);
478: r.put(g.name, modules);
479: }
480: for (FileSet fs : g.filesets) {
481: DirectoryScanner ds = fs
482: .getDirectoryScanner(getProject());
483: for (String file : ds.getIncludedFiles()) {
484: File n_file = new File(fs.getDir(getProject()),
485: file);
486: try {
487: ZipFile zip = new ZipFile(n_file);
488: try {
489: ZipEntry entry = zip
490: .getEntry("Info/info.xml");
491: if (entry == null) {
492: throw new BuildException(
493: "NBM "
494: + n_file
495: + " was malformed: no Info/info.xml",
496: getLocation());
497: }
498: EntityResolver nullResolver = new EntityResolver() {
499: public InputSource resolveEntity(
500: String publicId, String systemId)
501: throws SAXException,
502: IOException {
503: return new InputSource(
504: new ByteArrayInputStream(
505: new byte[0]));
506: }
507: };
508: Module m = new Module();
509: InputStream is = zip.getInputStream(entry);
510: try {
511: m.xml = XMLUtil.parse(
512: new InputSource(is), false,
513: false, null, nullResolver)
514: .getDocumentElement();
515: } finally {
516: is.close();
517: }
518: m.nbm = n_file;
519: m.relativePath = file.replace(
520: File.separatorChar, '/');
521: Collection<Module> moduleCollection = modules;
522: Element manifest = ((Element) m.xml
523: .getElementsByTagName("manifest")
524: .item(0));
525: if (automaticGrouping && g.name == null) {
526: // insert modules with no explicit grouping into group acc. to manifest:
527: String categ = manifest
528: .getAttribute("OpenIDE-Module-Display-Category");
529: if (categ.length() > 0) {
530: moduleCollection = r.get(categ);
531: if (moduleCollection == null) {
532: moduleCollection = new TreeSet<Module>(
533: moduleDisplayNameComparator);
534: r.put(categ, moduleCollection);
535: }
536: }
537: }
538: boolean old = false; // #110661
539: String destDir = getProject().getProperty(
540: "netbeans.dest.dir");
541: if (destDir != null) {
542: for (File cluster : getProject()
543: .resolveFile(destDir)
544: .listFiles()) {
545: if (new File(cluster,
546: "modules/org-netbeans-modules-autoupdate.jar")
547: .isFile()) {
548: old = true;
549: break;
550: }
551: }
552: }
553: if (!old) {
554: String cnb = manifest.getAttribute(
555: "OpenIDE-Module").replaceFirst(
556: "/\\d+$", "");
557: entry = zip
558: .getEntry("netbeans/config/Modules/"
559: + cnb.replace('.', '-')
560: + ".xml");
561: if (entry != null) {
562: is = zip.getInputStream(entry);
563: try {
564: NodeList nl = XMLUtil.parse(
565: new InputSource(is),
566: false, false, null,
567: nullResolver)
568: .getElementsByTagName(
569: "param");
570: for (int i = 0; i < nl
571: .getLength(); i++) {
572: String name = ((Element) nl
573: .item(i))
574: .getAttribute("name");
575: String value = ((Text) nl
576: .item(i)
577: .getFirstChild())
578: .getData();
579: if (name.equals("autoload")
580: && value
581: .equals("true")) {
582: m.autoload = true;
583: }
584: if (name.equals("eager")
585: && value
586: .equals("true")) {
587: m.eager = true;
588: }
589: }
590: } finally {
591: is.close();
592: }
593: }
594: }
595: moduleCollection.add(m);
596: } finally {
597: zip.close();
598: }
599: } catch (Exception e) {
600: throw new BuildException(
601: "Cannot access nbm file: " + n_file, e,
602: getLocation());
603: }
604: }
605: }
606: }
607: return r;
608: }
609:
610: }
|