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.ByteArrayInputStream;
045: import java.io.ByteArrayOutputStream;
046: import java.io.DataInput;
047: import java.io.DataInputStream;
048: import java.io.File;
049: import java.io.IOException;
050: import java.io.InputStream;
051: import java.util.Arrays;
052: import java.util.Enumeration;
053: import java.util.HashMap;
054: import java.util.HashSet;
055: import java.util.Map;
056: import java.util.Set;
057: import java.util.TreeMap;
058: import java.util.concurrent.atomic.AtomicInteger;
059: import java.util.jar.Attributes;
060: import java.util.jar.JarEntry;
061: import java.util.jar.JarFile;
062: import java.util.jar.Manifest;
063: import java.util.regex.Pattern;
064: import org.apache.tools.ant.AntClassLoader;
065: import org.apache.tools.ant.BuildException;
066: import org.apache.tools.ant.Project;
067: import org.apache.tools.ant.Task;
068: import org.apache.tools.ant.types.Path;
069:
070: /**
071: * Verifies linkage between classes in a JAR (typically a module).
072: * @author Jesse Glick
073: * @see "#71675"
074: * @see <a href="http://java.sun.com/docs/books/vmspec/2nd-edition/html/ClassFile.doc.html">Class file spec</a>
075: */
076: public class VerifyClassLinkage extends Task {
077:
078: public VerifyClassLinkage() {
079: }
080:
081: /*
082: private boolean verifyMainJar = true;
083: private boolean verifyClassPathExtensions = true;
084: public void setVerifyClassPathExtensions(boolean verifyClassPathExtensions) {
085: this.verifyClassPathExtensions = verifyClassPathExtensions;
086: }
087: public void setVerifyMainJar(boolean verifyMainJar) {
088: this.verifyMainJar = verifyMainJar;
089: }
090: */
091:
092: private File jar;
093: private boolean failOnError = true;
094: private boolean warnOnDefaultPackage = true;
095: private Path classpath = new Path(getProject());
096: private String ignores;
097: private int maxWarnings = Integer.MAX_VALUE;
098:
099: /**
100: * Intended static classpath for this JAR.
101: * Any classes loaded in this JAR (and its Class-Path extensions)
102: * must be linkable against this classpath plus the JAR (and extensions) itself.
103: */
104: public Path createClasspath() {
105: return classpath.createPath();
106: }
107:
108: /**
109: * Specify the main JAR file.
110: * Automatically searches in Class-Path extensions too.
111: */
112: public void setJar(File jar) {
113: this .jar = jar;
114: }
115:
116: /**
117: * If true (default), halt build on error, rather than just
118: * reporting a warning.
119: */
120: public void setFailOnError(boolean failOnError) {
121: this .failOnError = failOnError;
122: }
123:
124: /**
125: * Sets the pattern for classes that are not verified.
126: * Allows to skip linkage verification of some classes.
127: */
128: public void setIgnores(String ignores) {
129: this .ignores = ignores;
130: }
131:
132: /**
133: * If true (default), warn if any classes are found in the default
134: * package. Never halts the build even if {@link #setFailOnError} true.
135: */
136: public void setWarnOnDefaultPackage(boolean warnOnDefaultPackage) {
137: this .warnOnDefaultPackage = warnOnDefaultPackage;
138: }
139:
140: /**
141: * Limit the number of warnings that will be generated in one task run.
142: * If there are more warnings than this, they will not be reported.
143: */
144: public void setMaxWarnings(int maxWarnings) {
145: if (maxWarnings <= 0) {
146: throw new IllegalArgumentException();
147: }
148: this .maxWarnings = maxWarnings;
149: }
150:
151: public @Override
152: void execute() throws BuildException {
153: if (jar == null) {
154: throw new BuildException("Must specify a JAR file",
155: getLocation());
156: }
157: try {
158: // Map from class name (foo/Bar format) to true (found), false (not found), null (as yet unknown):
159: Map<String, Boolean> loadable = new HashMap<String, Boolean>();
160: Map<String, byte[]> classfiles = new TreeMap<String, byte[]>();
161: read(jar, classfiles, new HashSet<File>());
162: for (String clazz : classfiles.keySet()) {
163: // All classes we define are obviously loadable:
164: loadable.put(clazz, Boolean.TRUE);
165: if (warnOnDefaultPackage && clazz.indexOf('.') == -1) {
166: log("Warning: class '" + clazz
167: + "' found in default package",
168: Project.MSG_WARN);
169: }
170: }
171: ClassLoader loader = new AntClassLoader(ClassLoader
172: .getSystemClassLoader().getParent(), getProject(),
173: classpath, true);
174: AtomicInteger max = new AtomicInteger(maxWarnings);
175: for (Map.Entry<String, byte[]> entry : classfiles
176: .entrySet()) {
177: String clazz = entry.getKey();
178: byte[] data = entry.getValue();
179: verify(clazz, data, loadable, loader, max);
180: if (max.get() < 0) {
181: break;
182: }
183: }
184: } catch (IOException e) {
185: throw new BuildException("While verifying " + jar
186: + " or its Class-Path extensions: " + e, e,
187: getLocation());
188: }
189: }
190:
191: private void read(File jar, Map<String, byte[]> classfiles,
192: Set<File> alreadyRead) throws IOException {
193: if (!alreadyRead.add(jar)) {
194: log("Already read " + jar, Project.MSG_VERBOSE);
195: return;
196: }
197: log("Reading " + jar, Project.MSG_VERBOSE);
198: JarFile jf = new JarFile(jar);
199: Pattern p = (ignores != null) ? Pattern.compile(ignores) : null;
200: try {
201: Enumeration e = jf.entries();
202: while (e.hasMoreElements()) {
203: JarEntry entry = (JarEntry) e.nextElement();
204: String name = entry.getName();
205: if (!name.endsWith(".class")) {
206: continue;
207: }
208: String clazz = name.substring(0, name.length() - 6)
209: .replace('/', '.');
210: if (p != null && p.matcher(clazz).matches()) {
211: continue;
212: }
213: ByteArrayOutputStream baos = new ByteArrayOutputStream(
214: Math.max((int) entry.getSize(), 0));
215: InputStream is = jf.getInputStream(entry);
216: try {
217: byte[] buf = new byte[4096];
218: int read;
219: while ((read = is.read(buf)) != -1) {
220: baos.write(buf, 0, read);
221: }
222: } finally {
223: is.close();
224: }
225: classfiles.put(clazz, baos.toByteArray());
226: }
227: Manifest mf = jf.getManifest();
228: if (mf != null) {
229: String cp = mf.getMainAttributes().getValue(
230: Attributes.Name.CLASS_PATH);
231: if (cp != null) {
232: String[] uris = cp.trim().split("[, ]+");
233: for (int i = 0; i < uris.length; i++) {
234: File otherJar = new File(jar.toURI().resolve(
235: uris[i]));
236: if (otherJar.isFile()) {
237: read(otherJar, classfiles, alreadyRead);
238: }
239: }
240: }
241: }
242: } finally {
243: jf.close();
244: }
245: }
246:
247: private static void skip(DataInput input, int bytes)
248: throws IOException {
249: int skipped = input.skipBytes(bytes);
250: if (skipped != bytes) {
251: throw new IOException("Truncated class file");
252: }
253: }
254:
255: private void verify(String clazz, byte[] data,
256: Map<String, Boolean> loadable, ClassLoader loader,
257: AtomicInteger maxWarn) throws IOException, BuildException {
258: //log("Verifying linkage of " + clazz.replace('/', '.'), Project.MSG_DEBUG);
259: DataInput input = new DataInputStream(new ByteArrayInputStream(
260: data));
261: skip(input, 8); // magic, minor_version, major_version
262: int size = input.readUnsignedShort() - 1; // constantPoolCount
263: String[] utf8Strings = new String[size];
264: boolean[] isClassName = new boolean[size];
265: for (int i = 0; i < size; i++) {
266: byte tag = input.readByte();
267: switch (tag) {
268: case 1: // CONSTANT_Utf8
269: utf8Strings[i] = input.readUTF();
270: break;
271: case 7: // CONSTANT_Class
272: int index = input.readUnsignedShort() - 1;
273: if (index >= size) {
274: throw new IOException("CONSTANT_Class index "
275: + index + " too big for size of pool "
276: + size);
277: }
278: //log("Class reference at " + index, Project.MSG_DEBUG);
279: isClassName[index] = true;
280: break;
281: case 3: // CONSTANT_Integer
282: case 4: // CONSTANT_Float
283: case 9: // CONSTANT_Fieldref
284: case 10: // CONSTANT_Methodref
285: case 11: // CONSTANT_InterfaceMethodref
286: case 12: // CONSTANT_NameAndType
287: skip(input, 4);
288: break;
289: case 8: // CONSTANT_String
290: skip(input, 2);
291: break;
292: case 5: // CONSTANT_Long
293: case 6: // CONSTANT_Double
294: skip(input, 8);
295: i++; // weirdness in spec
296: break;
297: default:
298: throw new IOException("Unrecognized constant pool tag "
299: + tag + " at index " + i
300: + "; running UTF-8 strings: "
301: + Arrays.asList(utf8Strings));
302: }
303: }
304: log("UTF-8 strings: " + Arrays.asList(utf8Strings),
305: Project.MSG_DEBUG);
306: for (int i = 0; i < size; i++) {
307: if (!isClassName[i]) {
308: continue;
309: }
310: String vmname = utf8Strings[i];
311: while (vmname.charAt(0) == '[') {
312: // array type
313: vmname = vmname.substring(1);
314: }
315: if (vmname.length() == 1) {
316: // primitive
317: continue;
318: }
319: String clazz2;
320: if (vmname.charAt(vmname.length() - 1) == ';'
321: && vmname.charAt(0) == 'L') {
322: // Uncommon but seems sometimes this happens.
323: clazz2 = vmname.substring(1, vmname.length() - 1);
324: } else {
325: clazz2 = vmname;
326: }
327: Boolean exists = loadable.get(clazz2.replace('/', '.'));
328: if (exists == null) {
329: exists = loader.getResource(clazz2 + ".class") != null;
330: loadable.put(clazz2, exists);
331: }
332: if (!exists) {
333: String message = clazz + " cannot access "
334: + clazz2.replace('/', '.');
335: if (failOnError) {
336: throw new BuildException(message, getLocation());
337: } else if (maxWarn.getAndDecrement() > 0) {
338: log("Warning: " + message, Project.MSG_WARN);
339: } else {
340: log("(additional warnings not reported)",
341: Project.MSG_WARN);
342: return;
343: }
344: } else {
345: //log("Working reference to " + clazz2.replace('/', '.'), Project.MSG_DEBUG);
346: }
347: }
348: }
349:
350: }
|