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-2007 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.File;
045: import java.io.IOException;
046: import java.io.StringReader;
047: import java.net.URI;
048: import java.util.Arrays;
049: import java.util.HashMap;
050: import java.util.HashSet;
051: import java.util.LinkedHashMap;
052: import java.util.Map;
053: import java.util.Set;
054: import java.util.SortedMap;
055: import java.util.SortedSet;
056: import java.util.TreeSet;
057: import java.util.jar.Attributes;
058: import java.util.jar.Manifest;
059: import java.util.regex.Matcher;
060: import java.util.regex.Pattern;
061: import org.apache.tools.ant.AntClassLoader;
062: import org.apache.tools.ant.BuildException;
063: import org.apache.tools.ant.Project;
064: import org.apache.tools.ant.Task;
065: import org.apache.tools.ant.types.Path;
066: import org.w3c.dom.Attr;
067: import org.w3c.dom.Document;
068: import org.w3c.dom.Element;
069: import org.w3c.dom.NamedNodeMap;
070: import org.w3c.dom.NodeList;
071: import org.xml.sax.EntityResolver;
072: import org.xml.sax.InputSource;
073: import org.xml.sax.SAXException;
074:
075: /**
076: * Task which checks content of an update center to make sure module dependencies
077: * are internally consistent. Can optionally also check against a previous update
078: * center snapshot to make sure that updates of modules marked as newer (i.e. with
079: * newer specification versions) would result in a consistent snapshot as well.
080: * If there are any modules which cannot be loaded, the build fails with a description.
081: * <p>
082: * Actual NBMs are not downloaded. Everything necessary is present just in
083: * the update center XML descriptor.
084: * <p>
085: * You must specify a classpath to load the NB module system from.
086: * It should suffice to include those JARs in the NB platform cluster's <code>lib</code> folder.
087: * @author Jesse Glick
088: */
089: public final class VerifyUpdateCenter extends Task {
090:
091: public VerifyUpdateCenter() {
092: }
093:
094: private URI updates;
095:
096: public void setUpdates(File f) {
097: updates = f.toURI();
098: }
099:
100: public void setUpdatesURL(URI u) {
101: updates = u;
102: }
103:
104: private URI oldUpdates;
105:
106: public void setOldUpdates(File f) {
107: if (f.isFile()) {
108: oldUpdates = f.toURI();
109: } else {
110: log("No such file: " + f, Project.MSG_WARN);
111: }
112: }
113:
114: public void setOldUpdatesURL(URI u) {
115: if (u.toString().length() > 0) {
116: oldUpdates = u;
117: }
118: }
119:
120: private Set<String> disabledAutoloads;
121:
122: /**
123: * Comma/space-separated list of CNBs of autoloads which are expected to be disabled.
124: * Generally should refer to "placeholder" modules which need to be installed but have no runtime component.
125: * If specified (even if empty), any other autoloads which are not enabled result in a build failure.
126: */
127: public void setDisabledAutoloads(String s) {
128: disabledAutoloads = new HashSet<String>(Arrays.asList(s
129: .split("[, ]+")));
130: }
131:
132: private boolean checkAutoUpdateVisibility;
133:
134: /** Turn on to check that all regular modules are marked for AU as either visible or essential. */
135: public void setCheckAutoUpdateVisibility(
136: boolean checkAutoUpdateVisibility) {
137: this .checkAutoUpdateVisibility = checkAutoUpdateVisibility;
138: }
139:
140: private Path classpath = new Path(getProject());
141:
142: public void addConfiguredClasspath(Path p) {
143: classpath.append(p);
144: }
145:
146: private File reportFile;
147:
148: /** JUnit-format XML result file to generate, rather than halting the build. */
149: public void setReport(File report) {
150: this .reportFile = report;
151: }
152:
153: public @Override
154: void execute() throws BuildException {
155: if (updates == null) {
156: throw new BuildException("you must specify updates");
157: }
158: Map<String, String> pseudoTests = new LinkedHashMap<String, String>();
159: ClassLoader loader = new AntClassLoader(getProject(), classpath);
160: Set<Manifest> manifests = loadManifests(updates);
161: if (checkAutoUpdateVisibility) {
162: Set<String> requiredBySomeone = new HashSet<String>();
163: for (Manifest m : manifests) {
164: String deps = m.getMainAttributes().getValue(
165: "OpenIDE-Module-Module-Dependencies");
166: if (deps != null) {
167: String identifier = "[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*";
168: Matcher match = Pattern.compile(
169: identifier + "(\\." + identifier + ")*")
170: .matcher(deps);
171: while (match.find()) {
172: requiredBySomeone.add(match.group());
173: }
174: }
175: }
176: StringBuilder auVisibilityProblems = new StringBuilder();
177: String[] markers = { "autoload", "eager",
178: "AutoUpdate-Show-In-Client",
179: "AutoUpdate-Essential-Module" };
180: MODULE: for (Manifest m : manifests) {
181: String cnb = findCNB(m);
182: if (requiredBySomeone.contains(cnb)) {
183: continue;
184: }
185: Attributes attr = m.getMainAttributes();
186: for (String marker : markers) {
187: if ("true".equals(attr.getValue(marker))) {
188: continue MODULE;
189: }
190: }
191: auVisibilityProblems.append("\n" + cnb);
192: }
193: pseudoTests
194: .put(
195: "testAutoUpdateVisibility",
196: auVisibilityProblems.length() > 0 ? "Some regular modules (that no one depends on) neither AutoUpdate-Show-In-Client nor AutoUpdate-Essential-Module"
197: + auVisibilityProblems
198: : null);
199: }
200: checkForProblems(findInconsistencies(manifests, loader,
201: disabledAutoloads), "Inconsistency(ies) in " + updates,
202: "synchronicConsistency", pseudoTests);
203: if (pseudoTests.get("synchronicConsistency") == null) {
204: log(updates + " is internally consistent", Project.MSG_INFO);
205: if (oldUpdates != null) {
206: Map<String, Manifest> updated = new HashMap<String, Manifest>();
207: for (Manifest m : loadManifests(oldUpdates)) {
208: updated.put(findCNB(m), m);
209: }
210: if (!findInconsistencies(
211: new HashSet<Manifest>(updated.values()),
212: loader, null).isEmpty()) {
213: log(
214: oldUpdates
215: + " is already inconsistent, skipping update check",
216: Project.MSG_WARN);
217: JUnitReportWriter.writeReport(this , reportFile,
218: pseudoTests);
219: return;
220: }
221: SortedSet<String> updatedCNBs = new TreeSet<String>();
222: Set<String> newCNBs = new HashSet<String>();
223: for (Manifest m : manifests) {
224: String cnb = findCNB(m);
225: newCNBs.add(cnb);
226: boolean doUpdate = true;
227: Manifest old = updated.get(cnb);
228: if (old != null) {
229: String oldspec = old
230: .getMainAttributes()
231: .getValue(
232: "OpenIDE-Module-Specification-Version");
233: String newspec = m
234: .getMainAttributes()
235: .getValue(
236: "OpenIDE-Module-Specification-Version");
237: doUpdate = specGreaterThan(newspec, oldspec);
238: }
239: if (doUpdate) {
240: updated.put(cnb, m);
241: updatedCNBs.add(cnb);
242: }
243: }
244: SortedMap<String, SortedSet<String>> updateProblems = findInconsistencies(
245: new HashSet<Manifest>(updated.values()),
246: loader, null);
247: updateProblems.keySet().retainAll(newCNBs); // ignore problems in now-deleted modules
248: checkForProblems(updateProblems,
249: "Inconsistency(ies) in " + updates
250: + " relative to " + oldUpdates,
251: "diachronicConsistency", pseudoTests);
252: if (pseudoTests.get("diachronicConsistency") == null) {
253: log(oldUpdates + " after updating " + updatedCNBs
254: + " from " + updates
255: + " remains consistent");
256: }
257: }
258: }
259: JUnitReportWriter.writeReport(this , reportFile, pseudoTests);
260: }
261:
262: @SuppressWarnings("unchecked")
263: private SortedMap<String, SortedSet<String>> findInconsistencies(
264: Set<Manifest> manifests, ClassLoader loader,
265: Set<String> disabledAutoloads) throws BuildException {
266: try {
267: return (SortedMap) loader.loadClass(
268: "org.netbeans.core.startup.ConsistencyVerifier")
269: .getMethod("findInconsistencies", Set.class,
270: Set.class).invoke(null, manifests,
271: disabledAutoloads);
272: } catch (Exception x) {
273: throw new BuildException(x, getLocation());
274: }
275: }
276:
277: private Set<Manifest> loadManifests(URI u) throws BuildException {
278: try {
279: Document doc = XMLUtil.parse(new InputSource(u.toString()),
280: false, false, null, new EntityResolver() {
281: public InputSource resolveEntity(String pub,
282: String sys) throws SAXException,
283: IOException {
284: if (pub.contains("DTD Autoupdate Catalog")) {
285: return new InputSource(
286: new StringReader(""));
287: } else {
288: return null;
289: }
290: }
291: });
292: Set<Manifest> manifests = new HashSet<Manifest>();
293: NodeList nl = doc.getElementsByTagName("manifest");
294: for (int i = 0; i < nl.getLength(); i++) {
295: Element m = (Element) nl.item(i);
296: Manifest mani = new Manifest();
297: NamedNodeMap map = m.getAttributes();
298: for (int j = 0; j < map.getLength(); j++) {
299: Attr a = (Attr) map.item(j);
300: mani.getMainAttributes().putValue(a.getName(),
301: a.getValue());
302: }
303: Element module = (Element) m.getParentNode();
304: for (String pseudoAttr : new String[] { "autoload",
305: "eager" }) {
306: if (module.getAttribute(pseudoAttr).equals("true")) {
307: mani.getMainAttributes().putValue(pseudoAttr,
308: "true");
309: }
310: }
311: manifests.add(mani);
312: }
313: return manifests;
314: } catch (Exception x) {
315: throw new BuildException("Could not load " + u, x,
316: getLocation());
317: }
318: }
319:
320: private static String findCNB(Manifest m) {
321: String name = m.getMainAttributes().getValue("OpenIDE-Module");
322: if (name == null) {
323: throw new IllegalArgumentException();
324: }
325: return name.replaceFirst("/\\d+$", "");
326: }
327:
328: private static boolean specGreaterThan(String newspec,
329: String oldspec) {
330: if (newspec == null) {
331: return false;
332: }
333: if (oldspec == null) {
334: return true;
335: }
336: String[] olddigits = oldspec.split("\\.");
337: String[] newdigits = newspec.split("\\.");
338: int oldlen = olddigits.length;
339: int newlen = newdigits.length;
340: int max = Math.max(oldlen, newlen);
341: for (int i = 0; i < max; i++) {
342: int oldd = (i < oldlen) ? Integer.parseInt(olddigits[i])
343: : 0;
344: int newd = (i < newlen) ? Integer.parseInt(newdigits[i])
345: : 0;
346: if (oldd != newd) {
347: return newd > oldd;
348: }
349: }
350: return false;
351: }
352:
353: private void checkForProblems(
354: SortedMap<String, SortedSet<String>> problems, String msg,
355: String testName, Map<String, String> pseudoTests) {
356: if (!problems.isEmpty()) {
357: StringBuffer message = new StringBuffer(msg);
358: for (Map.Entry<String, SortedSet<String>> entry : problems
359: .entrySet()) {
360: message.append("\nProblems found for module "
361: + entry.getKey() + ": " + entry.getValue());
362: }
363: pseudoTests.put(testName, message.toString());
364: } else {
365: pseudoTests.put(testName, null);
366: }
367: }
368:
369: }
|