001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.tools.license;
023:
024: import java.io.File;
025: import java.io.FileFilter;
026: import java.io.FileInputStream;
027: import java.io.FileOutputStream;
028: import java.io.FileWriter;
029: import java.io.IOException;
030: import java.io.RandomAccessFile;
031: import java.io.SyncFailedException;
032: import java.net.URL;
033: import java.nio.ByteBuffer;
034: import java.nio.channels.FileChannel;
035: import java.util.ArrayList;
036: import java.util.Iterator;
037: import java.util.List;
038: import java.util.Map;
039: import java.util.TreeMap;
040: import java.util.logging.Level;
041: import java.util.logging.Logger;
042:
043: import javax.xml.parsers.DocumentBuilder;
044: import javax.xml.parsers.DocumentBuilderFactory;
045:
046: import org.w3c.dom.Document;
047: import org.w3c.dom.Element;
048: import org.w3c.dom.Node;
049: import org.w3c.dom.NodeList;
050:
051: /**
052: * A utility which scans all java source files in the cvs tree and validates
053: * the license header prior to the package statement for headers that match
054: * those declared in thirdparty/licenses/license-info.xml
055: *
056: * @author Scott.Stark@jboss.org
057: * @version $Revision: 57184 $
058: */
059: public class ValidateLicenseHeaders {
060: /** Used to strip out diffs due to copyright date ranges */
061: static final String COPYRIGHT_REGEX = "copyright\\s(\\(c\\))*\\s*[\\d]+(\\s*,\\s*[\\d]+|\\s*-\\s*[\\d]+)*";
062:
063: static final String DEFAULT_HEADER = "/*\n"
064: + " * JBoss, Home of Professional Open Source.\n"
065: + " * Copyright 2006, Red Hat Middleware LLC, and individual contributors\n"
066: + " * as indicated by the @author tags. See the copyright.txt file in the\n"
067: + " * distribution for a full listing of individual contributors.\n"
068: + " *\n"
069: + " * This is free software; you can redistribute it and/or modify it\n"
070: + " * under the terms of the GNU Lesser General Public License as\n"
071: + " * published by the Free Software Foundation; either version 2.1 of\n"
072: + " * the License, or (at your option) any later version.\n"
073: + " *\n"
074: + " * This software is distributed in the hope that it will be useful,\n"
075: + " * but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
076: + " * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n"
077: + " * Lesser General Public License for more details.\n"
078: + " *\n"
079: + " * You should have received a copy of the GNU Lesser General Public\n"
080: + " * License along with this software; if not, write to the Free\n"
081: + " * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA\n"
082: + " * 02110-1301 USA, or see the FSF site: http://www.fsf.org.\n"
083: + " */\n";
084: static Logger log = Logger.getLogger("ValidateCopyrightHeaders");
085: static boolean addDefaultHeader = false;
086: static FileFilter dotJavaFilter = new DotJavaFilter();
087: /**
088: * The term-headers from the thirdparty/license/license-info.xml
089: */
090: static TreeMap licenseHeaders = new TreeMap();
091: /**
092: * Java source files with no license header
093: */
094: static ArrayList noheaders = new ArrayList();
095: /**
096: * Java source files with a header that does not match one from licenseHeaders
097: */
098: static ArrayList invalidheaders = new ArrayList();
099: /** Total java source files seen */
100: static int totalCount;
101: /** Total out of date jboss headers seen */
102: static int jbossCount;
103:
104: /**
105: * ValidateLicenseHeaders jboss-src-root
106: * @param args
107: */
108: public static void main(String[] args) throws Exception {
109: if (args.length == 0 || args[0].startsWith("-h")) {
110: log
111: .info("Usage: ValidateLicenseHeaders [-addheader] jboss-src-root");
112: System.exit(1);
113: }
114: int rootArg = 0;
115: if (args.length == 2) {
116: if (args[0].startsWith("-add"))
117: addDefaultHeader = true;
118: else {
119: log.severe("Uknown argument: " + args[0]);
120: log
121: .info("Usage: ValidateLicenseHeaders [-addheader] jboss-src-root");
122: System.exit(1);
123:
124: }
125: rootArg = 1;
126: }
127:
128: File jbossSrcRoot = new File(args[rootArg]);
129: if (jbossSrcRoot.exists() == false) {
130: log.info("Src root does not exist, check "
131: + jbossSrcRoot.getAbsolutePath());
132: System.exit(1);
133: }
134:
135: URL u = Thread
136: .currentThread()
137: .getContextClassLoader()
138: .getResource(
139: "META-INF/services/javax.xml.parsers.DocumentBuilderFactory");
140: System.err.println(u);
141:
142: // Load the valid copyright statements for the licenses
143: File licenseInfo = new File(jbossSrcRoot,
144: "thirdparty/licenses/license-info.xml");
145: if (licenseInfo.exists() == false) {
146: log
147: .severe("Failed to find the thirdparty/licenses/license-info.xml under the src root");
148: System.exit(1);
149: }
150: DocumentBuilderFactory factory = DocumentBuilderFactory
151: .newInstance();
152: DocumentBuilder db = factory.newDocumentBuilder();
153: Document doc = db.parse(licenseInfo);
154: NodeList licenses = doc.getElementsByTagName("license");
155: for (int i = 0; i < licenses.getLength(); i++) {
156: Element license = (Element) licenses.item(i);
157: String key = license.getAttribute("id");
158: ArrayList headers = new ArrayList();
159: licenseHeaders.put(key, headers);
160: NodeList copyrights = license
161: .getElementsByTagName("terms-header");
162: for (int j = 0; j < copyrights.getLength(); j++) {
163: Element copyright = (Element) copyrights.item(j);
164: copyright.normalize();
165: String id = copyright.getAttribute("id");
166: // The id will be blank if there is no id attribute
167: if (id.length() == 0)
168: continue;
169: String text = getElementContent(copyright);
170: if (text == null)
171: continue;
172: // Replace all duplicate whitespace and '*' with a single space
173: text = text.replaceAll("[\\s*]+", " ");
174: if (text.length() == 1)
175: continue;
176:
177: text = text.toLowerCase().trim();
178: // Replace any copyright date0-date1,date2 with copyright ...
179: text = text.replaceAll(COPYRIGHT_REGEX, "...");
180: LicenseHeader lh = new LicenseHeader(id, text);
181: headers.add(lh);
182: }
183: }
184: log.fine(licenseHeaders.toString());
185:
186: File[] files = jbossSrcRoot.listFiles(dotJavaFilter);
187: log.info("Root files count: " + files.length);
188: processSourceFiles(files, 0);
189:
190: log.info("Processed " + totalCount);
191: log.info("Updated jboss headers: " + jbossCount);
192: // Files with no headers details
193: log.info("Files with no headers: " + noheaders.size());
194: FileWriter fw = new FileWriter("NoHeaders.txt");
195: for (Iterator iter = noheaders.iterator(); iter.hasNext();) {
196: File f = (File) iter.next();
197: fw.write(f.getAbsolutePath());
198: fw.write('\n');
199: }
200: fw.close();
201:
202: // Files with unknown headers details
203: log
204: .info("Files with invalid headers: "
205: + invalidheaders.size());
206: fw = new FileWriter("InvalidHeaders.txt");
207: for (Iterator iter = invalidheaders.iterator(); iter.hasNext();) {
208: File f = (File) iter.next();
209: fw.write(f.getAbsolutePath());
210: fw.write('\n');
211: }
212: fw.close();
213:
214: // License usage summary
215: log.info("Creating HeadersSummary.txt");
216: fw = new FileWriter("HeadersSummary.txt");
217: for (Iterator iter = licenseHeaders.entrySet().iterator(); iter
218: .hasNext();) {
219: Map.Entry entry = (Map.Entry) iter.next();
220: String key = (String) entry.getKey();
221: fw.write("+++ License type=" + key);
222: fw.write('\n');
223: List list = (List) entry.getValue();
224: Iterator jiter = list.iterator();
225: while (jiter.hasNext()) {
226: LicenseHeader lh = (LicenseHeader) jiter.next();
227: fw.write('\t');
228: fw.write(lh.id);
229: fw.write(", count=");
230: fw.write("" + lh.count);
231: fw.write('\n');
232: }
233: }
234: fw.close();
235: }
236:
237: /**
238: * Get all non-comment content from the element.
239: * @param element
240: * @return the concatenated text/cdata content
241: */
242: public static String getElementContent(Element element) {
243: if (element == null)
244: return null;
245:
246: NodeList children = element.getChildNodes();
247: StringBuffer result = new StringBuffer();
248: for (int i = 0; i < children.getLength(); i++) {
249: Node child = children.item(i);
250: if (child.getNodeType() == Node.TEXT_NODE
251: || child.getNodeType() == Node.CDATA_SECTION_NODE) {
252: result.append(child.getNodeValue());
253: } else if (child.getNodeType() == Node.COMMENT_NODE) {
254: // Ignore comment nodes
255: } else {
256: result.append(child.getFirstChild());
257: }
258: }
259: return result.toString().trim();
260: }
261:
262: /**
263: * Validate the headers of all java source files
264: *
265: * @param files
266: * @param level
267: * @throws IOException
268: */
269: static void processSourceFiles(File[] files, int level)
270: throws IOException {
271: for (int i = 0; i < files.length; i++) {
272: File f = files[i];
273: if (level == 0)
274: log.info("processing " + f);
275: if (f.isDirectory()) {
276: File[] children = f.listFiles(dotJavaFilter);
277: processSourceFiles(children, level + 1);
278: } else {
279: parseHeader(f);
280: }
281: }
282: }
283:
284: /**
285: * Read the first comment upto the package ...; statement
286: * @param javaFile
287: */
288: static void parseHeader(File javaFile) throws IOException {
289: totalCount++;
290: RandomAccessFile raf = new RandomAccessFile(javaFile, "rw");
291: String line = raf.readLine();
292: StringBuffer tmp = new StringBuffer();
293: long endOfHeader = 0;
294: boolean packageOrImport = false;
295: while (line != null) {
296: long nextEOH = raf.getFilePointer();
297: line = line.trim();
298: // Ignore any single line comments
299: if (line.startsWith("//")) {
300: line = raf.readLine();
301: continue;
302: }
303:
304: // If this is a package/import/class/interface statement break
305: if (line.startsWith("package") || line.startsWith("import")
306: || line.indexOf("class") >= 0
307: || line.indexOf("interface") >= 0) {
308: packageOrImport = true;
309: break;
310: }
311:
312: // Update the current end of header marker
313: endOfHeader = nextEOH;
314:
315: if (line.startsWith("/**"))
316: tmp.append(line.substring(3));
317: else if (line.startsWith("/*"))
318: tmp.append(line.substring(2));
319: else if (line.startsWith("*"))
320: tmp.append(line.substring(1));
321: else
322: tmp.append(line);
323: tmp.append(' ');
324: line = raf.readLine();
325: }
326: raf.close();
327:
328: if (tmp.length() == 0 || packageOrImport == false) {
329: addDefaultHeader(javaFile);
330: return;
331: }
332:
333: String text = tmp.toString();
334: // Replace all duplicate whitespace with a single space
335: text = text.replaceAll("[\\s*]+", " ");
336: text = text.toLowerCase().trim();
337: // Replace any copyright date0-date1,date2 with copyright ...
338: text = text.replaceAll(COPYRIGHT_REGEX, "...");
339: if (tmp.length() == 0) {
340: addDefaultHeader(javaFile);
341: return;
342: }
343: // Search for a matching header
344: boolean matches = false;
345: String matchID = null;
346: Iterator iter = licenseHeaders.entrySet().iterator();
347: escape: while (iter.hasNext()) {
348: Map.Entry entry = (Map.Entry) iter.next();
349: String key = (String) entry.getKey();
350: List list = (List) entry.getValue();
351: Iterator jiter = list.iterator();
352: while (jiter.hasNext()) {
353: LicenseHeader lh = (LicenseHeader) jiter.next();
354: if (text.startsWith(lh.text)) {
355: matches = true;
356: matchID = lh.id;
357: lh.count++;
358: lh.usage.add(javaFile);
359: if (log.isLoggable(Level.FINE))
360: log.fine(javaFile + " matches copyright key="
361: + key + ", id=" + lh.id);
362: break escape;
363: }
364: }
365: }
366: text = null;
367: tmp.setLength(0);
368: if (matches == false)
369: invalidheaders.add(javaFile);
370: else if (matchID.startsWith("jboss")
371: && matchID.endsWith("#0") == false) {
372: // This is a legacy jboss head that needs to be updated to the default
373: replaceHeader(javaFile, endOfHeader);
374: jbossCount++;
375: }
376: }
377:
378: /**
379: * Replace a legacy jboss header with the current default header
380: * @param javaFile - the java source file
381: * @param endOfHeader - the offset to the end of the legacy header
382: * @throws IOException - thrown on failure to replace the header
383: */
384: static void replaceHeader(File javaFile, long endOfHeader)
385: throws IOException {
386: if (log.isLoggable(Level.FINE))
387: log.fine("Replacing legacy jboss header in: " + javaFile);
388: RandomAccessFile raf = new RandomAccessFile(javaFile, "rw");
389: File bakFile = new File(javaFile.getAbsolutePath() + ".bak");
390: FileOutputStream fos = new FileOutputStream(bakFile);
391: fos.write(DEFAULT_HEADER.getBytes());
392: FileChannel fc = raf.getChannel();
393: long count = raf.length() - endOfHeader;
394: fc.transferTo(endOfHeader, count, fos.getChannel());
395: fc.close();
396: fos.close();
397: raf.close();
398: if (javaFile.delete() == false)
399: log.severe("Failed to delete java file: " + javaFile);
400: if (bakFile.renameTo(javaFile) == false)
401: throw new SyncFailedException("Failed to replace: "
402: + javaFile);
403: }
404:
405: /**
406: * Add the default jboss lgpl header
407: */
408: static void addDefaultHeader(File javaFile) throws IOException {
409: if (addDefaultHeader) {
410: FileInputStream fis = new FileInputStream(javaFile);
411: FileChannel fc = fis.getChannel();
412: int size = (int) fc.size();
413: ByteBuffer contents = ByteBuffer.allocate(size);
414: fc.read(contents);
415: fis.close();
416:
417: ByteBuffer hdr = ByteBuffer.wrap(DEFAULT_HEADER.getBytes());
418: FileOutputStream fos = new FileOutputStream(javaFile);
419: fos.write(hdr.array());
420: fos.write(contents.array());
421: fos.close();
422: }
423:
424: noheaders.add(javaFile);
425: }
426:
427: /**
428: * A class that encapsulates the license id and valid terms header
429: */
430: static class LicenseHeader {
431: String id;
432: String text;
433: int count;
434: ArrayList usage = new ArrayList();
435:
436: LicenseHeader(String id, String text) {
437: this .id = id;
438: this .text = text;
439: }
440: }
441:
442: /**
443: * A filter which accepts files ending in .java (but not _Stub.java), or
444: * directories other than gen-src and gen-parsers
445: */
446: static class DotJavaFilter implements FileFilter {
447: public boolean accept(File pathname) {
448: boolean accept = false;
449: String name = pathname.getName();
450: if (pathname.isDirectory()) {
451: // Ignore the gen-src directories for generated output
452: accept = name.equals("gen-src") == false
453: && name.equals("gen-parsers") == false;
454: } else {
455: accept = name.endsWith("_Stub.java") == false
456: && name.endsWith(".java");
457: }
458:
459: return accept;
460: }
461: }
462: }
|