changeset 21472:c190ed6b84bf

added checkcopyrights command (from mxtool2)
author Doug Simon <doug.simon@oracle.com>
date Fri, 22 May 2015 23:11:17 +0200
parents a64d09dc4590
children 923c37b10fb4
files mxtool/CheckCopyright.java mxtool/copyrights/oracle.copyright.hash mxtool/copyrights/oracle.copyright.regex.hash mxtool/copyrights/oracle.copyright.regex.star mxtool/copyrights/oracle.copyright.star mxtool/mx.py
diffstat 6 files changed, 1029 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mxtool/CheckCopyright.java	Fri May 22 23:11:17 2015 +0200
@@ -0,0 +1,918 @@
+/*
+ * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+import java.io.*;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.*;
+import java.util.regex.*;
+
+
+/**
+ * A program to check the existence and correctness of the copyright notice on a given set of sources.
+ * Sources are defined to be those under management by Mercurial and various options are available
+ * to limit the set of sources scanned.
+ */
+public class CheckCopyright {
+
+    static class YearInfo {
+
+        final int firstYear;
+        final int lastYear;
+
+        YearInfo(int firstYear, int lastYear) {
+            this.firstYear = firstYear;
+            this.lastYear = lastYear;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            final YearInfo yearInfo = (YearInfo) other;
+            return yearInfo.firstYear == firstYear && yearInfo.lastYear == lastYear;
+        }
+
+        @Override
+        public int hashCode() {
+            return firstYear ^ lastYear;
+        }
+    }
+
+    static class Info extends YearInfo {
+
+        final String fileName;
+
+        Info(String fileName, int firstYear, int lastYear) {
+            super(firstYear, lastYear);
+            this.fileName = fileName;
+        }
+
+        @Override
+        public String toString() {
+            return fileName + " " + firstYear + ", " + lastYear;
+        }
+    }
+
+    private static abstract class CopyrightHandler {
+    	enum CommentType{
+    		STAR, HASH
+    	}
+
+        private static Map<String, CopyrightHandler> copyrightMap;
+        private static String copyrightFiles = ".*/makefile|.*/Makefile|.*\\.sh|.*\\.bash|.*\\.mk|.*\\.java|.*\\.c|.*\\.h|.*\\.py|.*\\.g|.*\\.r";
+        private static Pattern copyrightFilePattern;
+
+        protected final String suffix;
+        private CopyrightHandler customHandler;
+
+        CopyrightHandler(CommentType commentType) {
+            this.suffix = commentType.name().toLowerCase();
+            initCopyrightMap();
+        }
+
+        void addCustomhandler(CopyrightHandler copyrightHandler) {
+        	this.customHandler = copyrightHandler;
+        }
+
+        /**
+         * Add @code extension to files handled by this {@code CopyrightKind}
+         */
+        protected void updateMap(String extension) {
+        	copyrightMap.put(extension, this);
+        }
+
+        static void addCopyrightFilesPattern(String pattern) {
+            copyrightFiles += "|" + pattern;
+        }
+
+        protected abstract void readCopyrights()  throws IOException;
+
+        protected abstract Matcher getMatcher(String fileName, String fileContent) throws IOException;
+
+        protected abstract String getText(String fileName) throws IOException ;
+
+        protected abstract boolean handlesFile(String fileName);
+
+        /**
+         * Checks that the Oracle copyright year info was correct.
+         * @return {@code false} if the year info was incorrect and was not fixed otherwise return {@code true}
+         * @throws IOException
+         */
+        protected abstract boolean checkYearInfo(String fileName, String fileContent, Matcher matcher, Info info) throws IOException;
+
+        static String getCopyrightText(String fileName) throws IOException {
+        	return getCopyrightHandler(fileName).getText(fileName);
+        }
+
+        private static CopyrightHandler getCopyrightHandler(String fileName) {
+        	initCopyrightMap();
+            if (!copyrightFilePattern.matcher(fileName).matches()) {
+                return null;
+            }
+            CopyrightHandler ck = getDefaultHandler(fileName);
+            if (ck.customHandler != null && ck.customHandler.handlesFile(fileName)) {
+            	return ck.customHandler;
+            } else {
+            	return ck;
+            }
+        }
+
+        private static void initCopyrightMap() {
+            if (copyrightMap == null) {
+                copyrightMap = new HashMap<String, CopyrightHandler>();
+                copyrightFilePattern = Pattern.compile(copyrightFiles);
+            }
+        }
+
+        static CopyrightHandler getDefaultHandler(String fileName) {
+            int index = fileName.lastIndexOf(File.separatorChar);
+            if (index > 0) {
+                fileName = fileName.substring(index + 1);
+            }
+            String ext = "";
+            index = fileName.lastIndexOf('.');
+            if (index > 0) {
+                ext = fileName.substring(index + 1);
+            }
+            if (fileName.equals("makefile")) {
+                ext = "mk";
+            }
+            CopyrightHandler ck = copyrightMap.get(ext);
+            assert ck != null : fileName;
+        	return ck;
+        }
+
+        protected String readCopyright(InputStream is) throws IOException {
+            byte[] b = new byte[16384];
+            int n = is.read(b);
+            is.close();
+            return new String(b, 0, n);
+    	}
+
+    }
+
+    private static class DefaultCopyrightHandler extends CopyrightHandler {
+        private static String ORACLE_COPYRIGHT = "oracle.copyright";
+        private static String ORACLE_COPYRIGHT_REGEX = "oracle.copyright.regex";
+
+        private String copyrightRegex;
+        private String copyright;
+        Pattern copyrightPattern;
+
+        DefaultCopyrightHandler(CopyrightHandler.CommentType commentType) throws IOException {
+    		super(commentType);
+    		if (commentType == CopyrightHandler.CommentType.STAR) {
+    			updateMap("java");
+    			updateMap("c");
+    			updateMap("h");
+    			updateMap("g");
+    		} else {
+    			updateMap("r");
+    			updateMap("R");
+    			updateMap("py");
+    			updateMap("sh");
+    			updateMap("mk");
+    			updateMap("bash");
+    			updateMap("");
+    		}
+    		readCopyrights();
+    	}
+
+    	private String readCopyright(String name) throws IOException {
+    		String copyRightDir = COPYRIGHT_DIR.getValue();
+    		String fileName = "copyrights/" + name + "." + suffix;
+    		String copyrightPath;
+    		if (copyRightDir != null) {
+    			copyrightPath = new File(new File(copyRightDir), fileName).getAbsolutePath();
+    		} else {
+    			URL url = CheckCopyright.class.getResource(fileName);
+    			try {
+    			copyrightPath = url.toURI().getPath();
+    			} catch (URISyntaxException ex) {
+    				throw new IOException(ex);
+    			}
+    		}
+            InputStream is = new FileInputStream(copyrightPath);
+            return readCopyright(is);
+        }
+
+    	@Override
+        protected void readCopyrights()  throws IOException {
+         	copyright = readCopyright(ORACLE_COPYRIGHT);
+         	copyrightRegex =  readCopyright(ORACLE_COPYRIGHT_REGEX);
+         	copyrightPattern = Pattern.compile(copyrightRegex, Pattern.DOTALL);
+        }
+
+    	@Override
+    	protected Matcher getMatcher(String fileName, String fileContent) {
+            return copyrightPattern.matcher(fileContent);
+    	}
+
+    	@Override
+        protected String getText(String fileName) {
+    		return copyright;
+    	}
+
+    	@Override
+    	protected boolean handlesFile(String fileName) {
+    		return true;
+    	}
+
+    	/**
+    	 * Check the year info against the copyright header.
+    	 * N.B. In the case of multiple matching groups, only the last group is checked.
+    	 * I.e., only the last lines containing year info is checked/updated.
+    	 */
+    	@Override
+        protected boolean checkYearInfo(String fileName, String fileContent, Matcher matcher, Info info) throws IOException {
+            int yearInCopyright;
+            int yearInCopyrightIndex;
+            int groupCount = matcher.groupCount();
+            String yearInCopyrightString = matcher.group(groupCount);
+            yearInCopyright = Integer.parseInt(yearInCopyrightString);
+            yearInCopyrightIndex = matcher.start(groupCount);
+            if (yearInCopyright != info.lastYear) {
+                System.out.println(fileName + " copyright last modified year " + yearInCopyright + ", hg last modified year " + info.lastYear);
+                if (FIX.getValue()) {
+                    // Use currentYear as that is what it will be when it's checked in!
+                    System.out.println("updating last modified year of " + fileName + " to " + info.lastYear);
+                    // If the previous copyright only specified a single (initial) year, we convert it to the pair form
+                    String newContent = fileContent.substring(0, yearInCopyrightIndex);
+                    if (matcher.group(groupCount - 1) == null) {
+                    	// single year form
+                    	newContent += yearInCopyrightString + ", ";
+                    }
+                    newContent += info.lastYear + fileContent.substring(yearInCopyrightIndex + 4);
+                    final FileOutputStream os = new FileOutputStream(fileName);
+                    os.write(newContent.getBytes());
+                    os.close();
+                    return true;
+                } else {
+                	return false;
+                }
+            }
+            return true;
+    	}
+
+    }
+
+    private static class CustomCopyrightHandler extends CopyrightHandler {
+    	private Map<String, String> overrides = new HashMap<String, String>();
+    	private CopyrightHandler defaultHandler;
+
+    	CustomCopyrightHandler(CopyrightHandler.CommentType commentType, CopyrightHandler defaultHandler) {
+    		super(commentType);
+    		this.defaultHandler = defaultHandler;
+    	}
+
+    	void addFile(String fileName, String copyright) {
+    		overrides.put(fileName, copyright);
+    	}
+
+		@Override
+		protected void readCopyrights() throws IOException {
+		}
+
+		@Override
+		protected Matcher getMatcher(String fileName, String fileContent) throws IOException {
+			String copyright = overrides.get(fileName);
+			assert copyright != null : fileName;
+			try (InputStream fs = new FileInputStream(copyright + "." + suffix + ".regex")) {
+				return Pattern.compile(readCopyright(fs), Pattern.DOTALL).matcher(fileContent);
+			}
+		}
+
+		@Override
+		protected String getText(String fileName) throws IOException {
+			String copyright = overrides.get(fileName);
+			assert copyright != null : fileName;
+			try (InputStream fs = new FileInputStream(copyright + "." + suffix)) {
+				return readCopyright(fs);
+			}
+		}
+
+		@Override
+		protected boolean handlesFile(String fileName) {
+			return overrides.get(fileName) != null;
+		}
+
+		@Override
+        protected boolean checkYearInfo(String fileName, String fileContent, Matcher matcher, Info info) throws IOException {
+			// This is a bit tacky
+			String copyright = overrides.get(fileName);
+			if (copyright.endsWith("no.copyright")) {
+				return true;
+			}
+			return defaultHandler.checkYearInfo(fileName, fileContent, matcher, info);
+		}
+    }
+
+	private static void initCopyrightKinds() throws IOException {
+		CopyrightHandler starHandler = new DefaultCopyrightHandler(CopyrightHandler.CommentType.STAR);
+		CopyrightHandler hashHandler = new DefaultCopyrightHandler(CopyrightHandler.CommentType.HASH);
+
+		String customCopyrightDir = CUSTOM_COPYRIGHT_DIR.getValue();
+		if (customCopyrightDir != null) {
+			CustomCopyrightHandler customStarHandler = new CustomCopyrightHandler(CopyrightHandler.CommentType.STAR, starHandler);
+			CustomCopyrightHandler customHashHandler = new CustomCopyrightHandler(CopyrightHandler.CommentType.HASH, hashHandler);
+			starHandler.addCustomhandler(customStarHandler);
+			hashHandler.addCustomhandler(customHashHandler);
+
+			File overrides = new File(new File(customCopyrightDir), "overrides");
+			if (overrides.exists()) {
+				ArrayList<String> lines = new ArrayList<>();
+				boolean changed = false;
+				try (BufferedReader br = new BufferedReader(new FileReader(
+						overrides))) {
+					while (true) {
+						String line = br.readLine();
+						if (line == null) {
+							break;
+						}
+						if (line.length() == 0 || line.startsWith("#")) {
+							lines.add(line);
+							continue;
+						}
+						String[] parts = line.split(",");
+						// filename,copyright-file
+						CopyrightHandler defaultHandler = CopyrightHandler.getDefaultHandler(parts[0]);
+						if (defaultHandler == null) {
+							System.err.println("no default copyright handler for: " + parts[0]);
+							System.exit(1);
+						}
+						if (!new File(parts[0]).exists()) {
+							System.err.printf("file %s in overrides file does not exist", parts[0]);
+							if (FIX.getValue()) {
+								System.err.print(" - removing");
+								line = null;
+								changed = true;
+							}
+							System.err.println();
+						}
+						if (line != null) {
+							lines.add(line);
+						}
+						CustomCopyrightHandler customhandler = (CustomCopyrightHandler) defaultHandler.customHandler;
+						customhandler.addFile(parts[0], new File(new File(customCopyrightDir), parts[1]).getAbsolutePath());
+					}
+				}
+				if (changed) {
+					try (BufferedWriter bw = new BufferedWriter(new FileWriter(
+							overrides))) {
+						for (String line : lines) {
+							bw.write(line);
+							bw.write('\n');
+						}
+					}
+				}
+			}
+		}
+	}
+
+    private static int currentYear = Calendar.getInstance().get(Calendar.YEAR);
+    private static Options options = new Options();
+    private static Option<Boolean> help = options.newBooleanOption("help", false, "Show help message and exit.");
+    private static Option<String> COPYRIGHT_DIR = options.newStringOption("copyright-dir", null, "override default location of copyright files");
+    private static Option<List<String>> FILES_TO_CHECK = options.newStringListOption("files", null, "list of files to check");
+    private static Option<String> FILE_LIST = options.newStringOption("file-list", null, "file containing list of files to check");
+    private static Option<Boolean> DIR_WALK = options.newBooleanOption("list-dir", false, "check all files in directory tree requiring a copyright (ls -R)");
+    private static Option<Boolean> HG_ALL = options.newBooleanOption("hg-all", false, "check all hg managed files requiring a copyright (hg status --all)");
+    private static Option<Boolean> HG_MODIFIED = options.newBooleanOption("hg-modified", false, "check all modified hg managed files requiring a copyright (hg status)");
+    private static Option<Boolean> HG_OUTGOING = options.newBooleanOption("hg-outgoing", false, "check outgoing hg managed files requiring a copyright (hg outgoing)");
+    private static Option<String> HG_LOG = options.newStringOption("hg-last", "0", "check hg managed files requiring a copyright in last N changesets (hg log -l N)");
+    private static Option<List<String>> PROJECT = options.newStringListOption("projects", null, "filter files to specific projects");
+    private static Option<String> OUTGOING_REPO = options.newStringOption("hg-repo", null, "override outgoing repository");
+    private static Option<Boolean> EXHAUSTIVE = options.newBooleanOption("hg-exhaustive", false, "check all hg managed files");
+    private static Option<Boolean> FIX = options.newBooleanOption("fix", false, "fix all copyright errors");
+    private static Option<String> FILE_PATTERN = options.newStringOption("file-pattern", null, "append additional file patterns for copyright checks");
+    private static Option<Boolean> REPORT_ERRORS = options.newBooleanOption("report-errors", false, "report non-fatal errors");
+    private static Option<Boolean> HALT_ON_ERROR = options.newBooleanOption("halt-on-error", false, "continue after normally fatal error");
+    private static Option<String> HG_PATH = options.newStringOption("hg-path", "hg", "path to hg executable");
+    private static Option<Boolean> VERBOSE = options.newBooleanOption("verbose", false, "verbose output");
+    private static Option<Boolean> VERY_VERBOSE = options.newBooleanOption("very-verbose", false, "very verbose output");
+    private static Option<String> CUSTOM_COPYRIGHT_DIR = options.newStringOption("custom-copyright-dir", null, "file containing filenames with custom copyrights");
+
+    private static String CANNOT_FOLLOW_FILE = "abort: cannot follow";
+    private static String hgPath;
+    private static boolean error;
+//    private static File workSpaceDirectory;
+    private static boolean verbose;
+    private static boolean veryVerbose;
+
+    public static void main(String[] args) {
+        // parse the arguments
+        options.parseArguments(args);
+        if (help.getValue()) {
+            options.printHelp();
+            return;
+        }
+
+        verbose = VERBOSE.getValue();
+        veryVerbose = VERY_VERBOSE.getValue();
+
+        hgPath = HG_PATH.getValue();
+
+        if (FILE_PATTERN.getValue() != null) {
+            CopyrightHandler.addCopyrightFilesPattern(FILE_PATTERN.getValue());
+        }
+
+        try {
+           initCopyrightKinds();
+            List<String> filesToCheck = null;
+            if (HG_ALL.getValue()) {
+                filesToCheck = getAllFiles(true);
+            } else if (HG_OUTGOING.getValue()) {
+                filesToCheck = getOutgoingFiles();
+            } else if (HG_MODIFIED.getValue()) {
+                filesToCheck = getAllFiles(false);
+            } else if (Integer.parseInt(HG_LOG.getValue()) > 0) {
+                filesToCheck = getLastNFiles(Integer.parseInt(HG_LOG.getValue()));
+            } else if (FILE_LIST.getValue() != null) {
+                filesToCheck = readFileList(FILE_LIST.getValue());
+            } else if (DIR_WALK.getValue()) {
+            	filesToCheck = getDirWalkFiles();
+            } else if (FILES_TO_CHECK.getValue() != null) {
+                filesToCheck = FILES_TO_CHECK.getValue();
+            } else {
+            	// no option set, default to HG_ALL
+            	filesToCheck = getAllFiles(true);
+            }
+            if (filesToCheck != null && filesToCheck.size() > 0) {
+                processFiles(filesToCheck);
+            } else {
+                System.out.println("nothing to check");
+            }
+            System.exit(error ? 1 : 0);
+        } catch (Exception ex) {
+            System.err.println("processing failed: " + ex);
+            ex.printStackTrace();
+        }
+    }
+
+    private static void processFiles(List<String> fileNames) throws Exception {
+        final List<String> projects = PROJECT.getValue();
+        Calendar cal = Calendar.getInstance();
+        for (String fileName : fileNames) {
+            if (projects == null || isInProjects(fileName, projects)) {
+            	File file = new File(fileName);
+            	if (file.isDirectory()) {
+            		continue;
+            	}
+            	if (verbose) {
+            		System.out.println("checking " + fileName);
+            	}
+                try {
+                	Info info = null;
+                	if (DIR_WALK.getValue()) {
+                		info = getFromLastModified(cal, fileName);
+                	} else {
+                		final List<String> logInfo = hglog(fileName);
+                		if (logInfo.size() == 0) {
+                			// an added file, so go with last modified
+                			info = getFromLastModified(cal, fileName);
+                		} else {
+                			info = getInfo(fileName, true, logInfo);
+                		}
+                	}
+                    checkFile(fileName, info);
+                } catch (Exception e) {
+                    System.err.format("COPYRIGHT CHECK WARNING: error while processing %s: %s%n", fileName, e.getMessage());
+                }
+            }
+        }
+    }
+
+    private static Info getFromLastModified(Calendar cal, String fileName) {
+		File file = new File(fileName);
+		cal.setTimeInMillis(file.lastModified());
+		int year = cal.get(Calendar.YEAR);
+		return new Info(fileName, year, year);
+    }
+
+    private static boolean isInProjects(String fileName, List<String> projects) {
+        final int ix = fileName.indexOf(File.separatorChar);
+        if (ix < 0) {
+            return false;
+        }
+        final String fileProject = fileName.substring(0, ix);
+        for (String project : projects) {
+            if (fileProject.equals(project)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static List<String> readFileList(String fileListName) throws IOException {
+        final List<String> result = new ArrayList<String>();
+        BufferedReader b = null;
+        try {
+            b = new BufferedReader(new FileReader(fileListName));
+            while (true) {
+                final String fileName = b.readLine();
+                if (fileName == null) {
+                    break;
+                }
+                if (fileName.length() == 0) {
+                    continue;
+                }
+                result.add(fileName);
+            }
+        } finally {
+            if (b != null) {
+                b.close();
+            }
+        }
+        return result;
+    }
+
+    private static Info getInfo(String fileName, boolean lastOnly, List<String> logInfo) {
+        // process sequence of changesets
+        int lastYear = 0;
+        int firstYear = 0;
+        int ix = 0;
+
+        while (ix < logInfo.size()) {
+        	Map<String, String> tagMap = new HashMap<>();
+        	ix = getChangeset(logInfo, ix, tagMap);
+        	String date = tagMap.get("date");
+            assert date != null;
+            final int csYear = getYear(date);
+            if (lastYear == 0) {
+                lastYear = csYear;
+                firstYear = lastYear;
+            } else {
+                firstYear = csYear;
+            }
+            // if we only want the last modified year, quit now
+            if (lastOnly) {
+                break;
+            }
+
+        }
+
+        if (HG_MODIFIED.getValue()) {
+            // We are only looking at modified and, therefore, uncommitted files.
+            // This means that the lastYear value will be the current year once the
+            // file is committed, so that is what we want to check against.
+            lastYear = currentYear;
+        }
+        return new Info(fileName, firstYear, lastYear);
+    }
+
+    /**
+     * Process all the changeset data, storing in {@outMap}.
+     * Return updated value of {@code ix}.
+     */
+    private static int getChangeset(List<String> logInfo, int ixx, Map<String, String> outMap) {
+    	int ix = ixx;
+    	String s = logInfo.get(ix++);
+    	while (s.length() > 0) {
+    		int cx = s.indexOf(':');
+    		String tag = s.substring(0, cx);
+    		String value = s.substring(cx + 1);
+    		outMap.put(tag, value);
+    		s = logInfo.get(ix++);
+    	}
+    	return ix;
+    }
+
+    private static int getYear(String dateLine) {
+        final String[] parts = dateLine.split(" ");
+        assert parts[parts.length - 2].startsWith("20");
+        return Integer.parseInt(parts[parts.length - 2]);
+    }
+
+    private static void checkFile(String c, Info info) throws IOException {
+        String fileName = info.fileName;
+        File file = new File(fileName);
+        if (!file.exists()) {
+            System.err.println("COPYRIGHT CHECK WARNING: file " + file + " doesn't exist");
+            return;
+        }
+        int fileLength = (int) file.length();
+        byte[] fileContentBytes = new byte[fileLength];
+        FileInputStream is = new FileInputStream(file);
+        is.read(fileContentBytes);
+        is.close();
+        final String fileContent = new String(fileContentBytes);
+        CopyrightHandler copyrightHandler = CopyrightHandler.getCopyrightHandler(fileName);
+        if (file.getName().equals("Makefile")) {
+        	System.console();
+        }
+        if (copyrightHandler != null) {
+            Matcher copyrightMatcher = copyrightHandler.getMatcher(fileName, fileContent);
+            if (copyrightMatcher.matches()) {
+            	error = error | !copyrightHandler.checkYearInfo(fileName, fileContent, copyrightMatcher, info);
+            } else {
+            	// If copyright is missing, insert it, otherwise user has to manually fix existing copyright.
+				if (!fileContent.contains("Copyright")) {
+					System.out.print("file " + fileName + " has missing copyright");
+					if (FIX.getValue()) {
+						final FileOutputStream os = new FileOutputStream(file);
+						os.write(CopyrightHandler.getCopyrightText(fileName)
+								.getBytes());
+						os.write(fileContentBytes);
+						os.close();
+						System.out.println("...fixed");
+					} else {
+						System.out.println();
+						error = true;
+					}
+				} else {
+					System.out.println("file " + fileName + " has malformed copyright" + (FIX.getValue() ? " not fixing" : ""));
+					error = true;
+				}
+            }
+        } else if (EXHAUSTIVE.getValue()) {
+            System.out.println("ERROR: file " + fileName + " has no copyright");
+            error = true;
+        }
+    }
+
+
+    private static List<String> hglog(String fileName) throws Exception {
+        final String[] cmd = new String[] {hgPath, "log", "-f", fileName};
+        return exec(null, cmd, true);
+    }
+
+    private static List<String> getLastNFiles(int n) throws Exception {
+        final String[] cmd = new String[] {hgPath, "log", "-v", "-l", Integer.toString(n)};
+        return getFilesFiles(exec(null, cmd, false));
+    }
+
+    private static List<String> getAllFiles(boolean all) throws Exception {
+        final String[] cmd;
+        if (HG_MODIFIED.getValue()) {
+            cmd = new String[] {hgPath,  "status"};
+        } else {
+            cmd = new String[] {hgPath,  "status",  "--all"};
+        }
+        List<String> output = exec(null, cmd, true);
+        final List<String> result = new ArrayList<String>(output.size());
+        for (String s : output) {
+            final char ch = s.charAt(0);
+            if (!(ch == 'R' || ch == 'I' || ch == '?' ||  ch == '!')) {
+                result.add(s.substring(2));
+            }
+        }
+        return result;
+    }
+
+    private static List<String> getOutgoingFiles() throws Exception {
+        final String[] cmd;
+        if (OUTGOING_REPO.getValue() == null) {
+            cmd = new String[] {hgPath,  "-v", "outgoing"};
+        } else {
+            cmd = new String[] {hgPath,  "-v", "outgoing", OUTGOING_REPO.getValue()};
+        }
+
+        final List<String> output = exec(null, cmd, false); // no outgoing exits with result 1
+        return getFilesFiles(output);
+    }
+
+    private static List<String> getFilesFiles(List<String> output) {
+        // there may be multiple changesets so merge the "files:"
+        final Map<String, String> outSet = new TreeMap<String, String>();
+        for (String s : output) {
+            if (s.startsWith("files:")) {
+                int ix = s.indexOf(' ');
+                while (ix < s.length() && s.charAt(ix) == ' ') {
+                    ix++;
+                }
+                final String[] files = s.substring(ix).split(" ");
+                for (String file : files) {
+                    outSet.put(file, file);
+                }
+            }
+        }
+        return new ArrayList<String>(outSet.values());
+    }
+
+    private static List<String> getDirWalkFiles() {
+    	File cwd = new File(System.getProperty("user.dir"));
+    	ArrayList<String> result = new ArrayList<String>();
+    	getDirWalkFiles(cwd, result);
+    	// remove "user.dir" prefix to make files relative as per hg
+    	String cwdPath = cwd.getAbsolutePath() + '/';
+    	for (int i = 0; i < result.size(); i++) {
+    		String path = result.get(i);
+    		result.set(i, path.replace(cwdPath, ""));
+    	}
+    	return result;
+    }
+
+    private static void getDirWalkFiles(File dir, ArrayList<String> list) {
+    	File[] files = dir.listFiles();
+    	for (File file : files) {
+    		if (ignoreFile(file.getName())) {
+    			continue;
+    		}
+    		if (file.isDirectory()) {
+    				getDirWalkFiles(file, list);
+    		} else {
+    			list.add(file.getAbsolutePath());
+    		}
+    	}
+    }
+
+    private static final String IGNORE_LIST = "\\.hg|.*\\.class|bin|src_gen";
+    private static final Pattern ignorePattern = Pattern.compile(IGNORE_LIST);
+
+    private static boolean ignoreFile(String name) {
+    	return ignorePattern.matcher(name).matches();
+    }
+
+    private static List<String> exec(File workingDir, String[] command, boolean failOnError) throws IOException, InterruptedException {
+        List<String> result = new ArrayList<String>();
+        if (veryVerbose) {
+            System.out.println("Executing process in directory: " + workingDir);
+            for (String c : command) {
+                System.out.println("  " + c);
+            }
+        }
+        final Process process = Runtime.getRuntime().exec(command, null, workingDir);
+        try {
+            result = readOutput(process.getInputStream());
+            final int exitValue = process.waitFor();
+            if (exitValue != 0) {
+                final List<String> errorResult = readOutput(process.getErrorStream());
+                if (REPORT_ERRORS.getValue()) {
+                    System.err.print("execution of command: ");
+                    for (String c : command) {
+                        System.err.print(c);
+                        System.err.print(' ');
+                    }
+                    System.err.println("failed with result " + exitValue);
+                    for (String e : errorResult) {
+                        System.err.println(e);
+                    }
+                }
+                if (failOnError && HALT_ON_ERROR.getValue()) {
+                	if (!cannotFollowNonExistentFile(errorResult)) {
+                		throw new Error("terminating");
+                	}
+                }
+            }
+        } finally {
+            process.destroy();
+        }
+        return result;
+    }
+
+    private static boolean cannotFollowNonExistentFile(List<String> errorResult) {
+        return errorResult.size() == 1 && errorResult.get(0).startsWith(CANNOT_FOLLOW_FILE);
+    }
+
+    private static List<String> readOutput(InputStream is) throws IOException {
+        final List<String> result = new ArrayList<String>();
+        BufferedReader bs = null;
+        try {
+            bs = new BufferedReader(new InputStreamReader(is));
+            while (true) {
+                final String line = bs.readLine();
+                if (line == null) {
+                    break;
+                }
+                result.add(line);
+            }
+        } finally {
+            if (bs != null) {
+                bs.close();
+            }
+        }
+        return result;
+    }
+
+    private static class Options {
+    	private static Map<String, Option<?>> optionMap  = new TreeMap<>();
+
+    	private Option<Boolean> newBooleanOption(String name, boolean defaultValue, String help) {
+    		Option<Boolean> option = new Option<Boolean>(name, help, defaultValue, false, false);
+    		optionMap.put(key(name), option);
+    		return option;
+    	}
+
+    	private Option<String> newStringOption(String name, String defaultValue, String help) {
+    		Option<String> option = new Option<String>(name, help, defaultValue);
+    		optionMap.put(key(name), option);
+    		return option;
+    	}
+
+    	private Option<List<String>> newStringListOption(String name, List<String> defaultValue, String help) {
+    		Option<List<String>> option = new Option<List<String>>(name, help, defaultValue, true, true);
+    		optionMap.put(key(name), option);
+    		return option;
+    	}
+
+    	private static String key(String name) {
+    		return "--" + name;
+    	}
+
+    	void parseArguments(String[] args) {
+            for (int i = 0; i < args.length; i++) {
+            	final String arg = args[i];
+            	if (arg.startsWith("--")) {
+            		Option<?> option = optionMap.get(arg);
+            		if (option == null || (option.consumesNext() && i == args.length - 1)) {
+            			System.out.println("usage:");
+            			printHelp();
+            			System.exit(1);
+            		}
+            		if (option.consumesNext()) {
+            			i++;
+            			option.setValue(args[i]);
+            		} else {
+            			option.setValue(true);
+            		}
+            	}
+            }
+    	}
+
+        void printHelp() {
+        	int maxKeyLen = 0;
+        	for (Map.Entry<String, Option<?>> entrySet : optionMap.entrySet()) {
+        		int l = entrySet.getKey().length();
+        		if (l > maxKeyLen) {
+        			maxKeyLen = l;
+        		}
+        	}
+        	for (Map.Entry<String, Option<?>> entrySet : optionMap.entrySet()) {
+        		String key = entrySet.getKey();
+        		System.out.printf("  %s", key);
+        		for (int i = 0; i < maxKeyLen - key.length(); i++) {
+        			System.out.print(' ');
+        		}
+        		System.out.printf("   %s%n", entrySet.getValue().help);
+        	}
+        }
+}
+
+    private static class Option<T> {
+    	private final String name;
+    	private final String help;
+    	private final boolean consumesNext;
+    	private final boolean isList;
+    	private T value;
+
+    	Option(String name, String help, T defaultValue, boolean consumesNext, boolean isList) {
+    		this.name = name;
+    		this.help = help;
+    		this.value = defaultValue;
+    		this.consumesNext = consumesNext;
+    		this.isList = isList;
+
+    	}
+
+    	Option(String name, String help, T defaultValue) {
+    		this(name, help, defaultValue, true, false);
+    	}
+
+        T getValue() {
+    		return value;
+    	}
+
+        boolean consumesNext() {
+        	return consumesNext;
+        }
+
+ 	   @SuppressWarnings("unchecked")
+       void setValue(boolean value) {
+        	this.value = (T) new Boolean(value);
+        }
+
+ 	   @SuppressWarnings("unchecked")
+        void setValue(String value) {
+ 		   if (isList) {
+ 			   String[] parts = value.split(",");
+ 			   this.value = (T) Arrays.asList(parts);
+ 		   } else {
+ 			   this.value = (T) value;
+ 		   }
+        }
+
+ 	   @SuppressWarnings("unused")
+ 	   String getName() {
+ 		   return name;
+ 	   }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mxtool/copyrights/oracle.copyright.hash	Fri May 22 23:11:17 2015 +0200
@@ -0,0 +1,22 @@
+#
+# Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+# or visit www.oracle.com if you need additional information or have any
+# questions.
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mxtool/copyrights/oracle.copyright.regex.hash	Fri May 22 23:11:17 2015 +0200
@@ -0,0 +1,1 @@
+(?:#!.*\n#\n#\ -*\n)?#\n# Copyright \(c\) (?:(20[0-9][0-9]), )?(20[0-9][0-9]), Oracle and/or its affiliates. All rights reserved.\n# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.\n#\n# This code is free software; you can redistribute it and/or modify it\n# under the terms of the GNU General Public License version 2 only, as\n# published by the Free Software Foundation.\n#\n# This code is distributed in the hope that it will be useful, but WITHOUT\n# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License\n# version 2 for more details \(a copy is included in the LICENSE file that\n# accompanied this code\).\n#\n# You should have received a copy of the GNU General Public License version\n# 2 along with this work; if not, write to the Free Software Foundation,\n# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\n#\n# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA\n# or visit www.oracle.com if you need additional information or have any\n# questions.\n#\n.*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mxtool/copyrights/oracle.copyright.regex.star	Fri May 22 23:11:17 2015 +0200
@@ -0,0 +1,1 @@
+/\*\n \* Copyright \(c\) (?:(20[0-9][0-9]), )?(20[0-9][0-9]), Oracle and/or its affiliates. All rights reserved.\n \* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.\n \*\n \* This code is free software; you can redistribute it and/or modify it\n \* under the terms of the GNU General Public License version 2 only, as\n \* published by the Free Software Foundation.\n \*\n \* This code is distributed in the hope that it will be useful, but WITHOUT\n \* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n \* FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License\n \* version 2 for more details \(a copy is included in the LICENSE file that\n \* accompanied this code\).\n \*\n \* You should have received a copy of the GNU General Public License version\n \* 2 along with this work; if not, write to the Free Software Foundation,\n \* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\n \*\n \* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA\n \* or visit www.oracle.com if you need additional information or have any\n \* questions.\n \*/\n.*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mxtool/copyrights/oracle.copyright.star	Fri May 22 23:11:17 2015 +0200
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
--- a/mxtool/mx.py	Fri May 22 23:10:51 2015 +0200
+++ b/mxtool/mx.py	Fri May 22 23:11:17 2015 +0200
@@ -2318,15 +2318,8 @@
 
     def _init_classpaths(self):
         if not self._classpaths_initialized:
-            myDir = dirname(__file__)
-            outDir = join(dirname(__file__), '.jdk' + str(self.version))
-            if not exists(outDir):
-                os.makedirs(outDir)
-            javaSource = join(myDir, 'ClasspathDump.java')
-            javaClass = join(outDir, 'ClasspathDump.class')
-            if not exists(javaClass) or getmtime(javaClass) < getmtime(javaSource):
-                subprocess.check_call([self.javac, '-d', _cygpathU2W(outDir), _cygpathU2W(javaSource)], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
-            self._bootclasspath, self._extdirs, self._endorseddirs = [x if x != 'null' else None for x in subprocess.check_output([self.java, '-cp', _cygpathU2W(outDir), 'ClasspathDump'], stderr=subprocess.PIPE).split('|')]
+            _, binDir = _compile_mx_class('ClasspathDump', jdk=self)
+            self._bootclasspath, self._extdirs, self._endorseddirs = [x if x != 'null' else None for x in subprocess.check_output([self.java, '-cp', _cygpathU2W(binDir), 'ClasspathDump'], stderr=subprocess.PIPE).split('|')]
             if self.javaCompliance <= JavaCompliance('1.8'):
                 # All 3 system properties accessed by ClasspathDump are expected to exist
                 if not self._bootclasspath or not self._extdirs or not self._endorseddirs:
@@ -2537,15 +2530,12 @@
 
     assert not path.endswith(os.sep)
 
-    myDir = dirname(__file__)
-    javaSource = join(myDir, 'URLConnectionDownload.java')
-    javaClass = join(myDir, 'URLConnectionDownload.class')
-    if not exists(javaClass) or getmtime(javaClass) < getmtime(javaSource):
-        subprocess.check_call([java().javac, '-d', _cygpathU2W(myDir), _cygpathU2W(javaSource)])
+    _, binDir = _compile_mx_class('URLConnectionDownload')
+
     verbose = []
     if sys.stderr.isatty():
         verbose.append("-v")
-    if run([java().java, '-cp', _cygpathU2W(myDir), 'URLConnectionDownload', _cygpathU2W(path)] + verbose + urls, nonZeroIsFatal=False) == 0:
+    if run([java().java, '-cp', _cygpathU2W(binDir), 'URLConnectionDownload', _cygpathU2W(path)] + verbose + urls, nonZeroIsFatal=False) == 0:
         return
 
     abort('Could not download to ' + path + ' from any of the following URLs:\n\n    ' +
@@ -5442,6 +5432,65 @@
         _show_section('projects', s.projects)
         _show_section('distributions', s.dists)
 
+def _compile_mx_class(javaClassName, classpath=None, jdk=None):
+    myDir = dirname(__file__)
+    binDir = join(myDir, 'bin' if not jdk else '.jdk' + str(jdk.version))
+    javaSource = join(myDir, javaClassName + '.java')
+    javaClass = join(binDir, javaClassName + '.class')
+    if not exists(javaClass) or getmtime(javaClass) < getmtime(javaSource):
+        if not exists(binDir):
+            os.mkdir(binDir)
+        javac = jdk.javac if jdk else java().javac
+        cmd = [javac, '-d', _cygpathU2W(binDir)]
+        if classpath:
+            cmd += ['-cp', _separatedCygpathU2W(binDir + os.pathsep + classpath)]
+        cmd += [_cygpathU2W(javaSource)]
+        try:
+            subprocess.check_call(cmd)
+        except subprocess.CalledProcessError:
+            abort('failed to compile:' + javaSource)
+
+    return (myDir, binDir)
+
+def checkcopyrights(args):
+    '''run copyright check on the sources'''
+    class CP(ArgumentParser):
+        def format_help(self):
+            return ArgumentParser.format_help(self) + self._get_program_help()
+
+        def _get_program_help(self):
+            help_output = subprocess.check_output([java().java, '-cp', _cygpathU2W(binDir), 'CheckCopyright', '--help'])
+            return '\nother argumemnts preceded with --\n' +  help_output
+
+    myDir, binDir = _compile_mx_class('CheckCopyright')
+
+    parser = CP(prog='mx checkcopyrights')
+
+    parser.add_argument('--primary', action='store_true', help='limit checks to primary suite')
+    parser.add_argument('remainder', nargs=REMAINDER, metavar='...')
+    args = parser.parse_args(args)
+    remove_doubledash(args.remainder)
+
+
+    # ensure compiled form of code is up to date
+
+    result = 0
+    # copyright checking is suite specific as each suite may have different overrides
+    for s in suites(True):
+        if args.primary and not s.primary:
+            continue
+        custom_copyrights = _cygpathU2W(join(s.mxDir, 'copyrights'))
+        custom_args = []
+        if exists(custom_copyrights):
+            custom_args = ['--custom-copyright-dir', custom_copyrights]
+        rc = run([java().java, '-cp', _cygpathU2W(binDir), 'CheckCopyright', '--copyright-dir', _cygpathU2W(myDir)] + custom_args + args.remainder, cwd=s.dir, nonZeroIsFatal=False)
+        result = result if rc == 0 else rc
+    return result
+
+def remove_doubledash(args):
+    if '--' in args:
+        args.remove('--')
+
 def ask_yes_no(question, default=None):
     """"""
     assert not default or default == 'y' or default == 'n'
@@ -5485,6 +5534,7 @@
     'build': [build, '[options]'],
     'checkstyle': [checkstyle, ''],
     'canonicalizeprojects': [canonicalizeprojects, ''],
+    'checkcopyrights': [checkcopyrights, '[options]'],
     'clean': [clean, ''],
     'eclipseinit': [eclipseinit, ''],
     'eclipseformat': [eclipseformat, ''],