Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1042)

Unified Diff: third_party/jmake/src/org/pantsbuild/jmake/PCDManager.java

Issue 1373723003: Fix javac --incremental by using jmake for dependency analysis (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@aidl
Patch Set: fix license check Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: third_party/jmake/src/org/pantsbuild/jmake/PCDManager.java
diff --git a/third_party/jmake/src/org/pantsbuild/jmake/PCDManager.java b/third_party/jmake/src/org/pantsbuild/jmake/PCDManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ff3cd10a95c18b8597a781c0010ab408206b6f1
--- /dev/null
+++ b/third_party/jmake/src/org/pantsbuild/jmake/PCDManager.java
@@ -0,0 +1,1603 @@
+/* Copyright (c) 2002-2008 Sun Microsystems, Inc. All rights reserved
+ *
+ * This program is distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+package org.pantsbuild.jmake;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.Adler32;
+
+/**
+ * This class implements management of the Project Class Directory, automatic tracking
+ * of changes and recompilation of .java sources for a project.
+ *
+ * @author Misha Dmitriev
+ * 23 January 2003
+ */
+public class PCDManager {
+
+ private PCDContainer pcdc;
+ private Map<String,PCDEntry> pcd; // Maps project class names to PCDEntries
+ private String projectJavaAndJarFilesArray[];
+ private String addedJavaAndJarFilesArray[], removedJavaAndJarFilesArray[], updatedJavaAndJarFilesArray[];
+ private List<String> newJavaFiles;
+ private Set<String> updatedJavaFiles;
+ private Set<String> recompiledJavaFiles;
+ private Set<String> updatedClasses; // This set is emptied on every new internal jmake iteration...
+ private Set<String> allUpdatedClasses; // whereas in this one the names of all updated classes found during this jmake invocation are stored.
+ private Set<String> updatedAndCheckedClasses;
+ private Set<String> deletedClasses;
+ private Set<String> updatedJarFiles;
+ private Set<String> stableJarFiles;
+ private Set<String> newJarFiles;
+ private Set<String> deletedJarFiles;
+ /* Dependencies from the dependencyFile, if any */
+ private Map<String, List<String>> extraDependencies;
+
+ private String destDir;
+ private boolean destDirSpecified;
+ private List<String> javacAddArgs;
+ private Class<?> compilerClass;
+ private Method compileMethod;
+ private String jcExecApp;
+ private Object externalApp;
+ private Method externalCompileSourceFilesMethod;
+ private Adler32 checkSum;
+ private CompatibilityChecker cv;
+ private ClassFileReader cfr;
+ private boolean newProject = false;
+ private String dependencyFile = null;
+ private static boolean backSlashFileSeparator = File.separatorChar != '/';
+
+ /**** Interface to the class ****/
+ /**
+ * Either projectJavaAndJarFilesArray != null and added.. == removed.. == updatedJavaAndJarFilesArray == null,
+ * or projectJavaAndJarFilesArray == null and one or more of others != null.
+ * When PCDManager is called from Main, this is guaranteed, since separate entrypoint functions initialize
+ * either one or another of the above argument groups, but never both.
+ */
+ public PCDManager(PCDContainer pcdc,
+ String projectJavaAndJarFilesArray[],
+ String addedJavaAndJarFilesArray[],
+ String removedJavaAndJarFilesArray[],
+ String updatedJavaAndJarFilesArray[],
+ String in_destDir,
+ List<String> javacAddArgs,
+ boolean failOnDependentJar,
+ boolean noWarnOnDependentJar,
+ String dependencyFile) {
+ this.pcdc = pcdc;
+ if (pcdc.pcd == null) {
+ pcd = new LinkedHashMap<String,PCDEntry>();
+ pcdc.pcd = pcd;
+ newProject = true;
+ } else {
+ pcd = pcdc.pcd;
+ }
+
+ this.projectJavaAndJarFilesArray = projectJavaAndJarFilesArray;
+ this.addedJavaAndJarFilesArray = addedJavaAndJarFilesArray;
+ this.removedJavaAndJarFilesArray = removedJavaAndJarFilesArray;
+ this.updatedJavaAndJarFilesArray = updatedJavaAndJarFilesArray;
+ this.dependencyFile = dependencyFile;
+ newJavaFiles = new ArrayList<String>();
+ updatedJavaFiles = new LinkedHashSet<String>();
+ recompiledJavaFiles = new LinkedHashSet<String>();
+ updatedAndCheckedClasses = new LinkedHashSet<String>();
+ deletedClasses = new LinkedHashSet<String>();
+ allUpdatedClasses = new LinkedHashSet<String>();
+
+ updatedJarFiles = new LinkedHashSet<String>();
+ stableJarFiles = new LinkedHashSet<String>();
+ newJarFiles = new LinkedHashSet<String>();
+ deletedJarFiles = new LinkedHashSet<String>();
+
+ initializeDestDir(in_destDir);
+ this.javacAddArgs = javacAddArgs;
+
+ checkSum = new Adler32();
+
+ cv = new CompatibilityChecker(this, failOnDependentJar, noWarnOnDependentJar);
+ cfr = new ClassFileReader();
+ }
+
+ public Collection<PCDEntry> entries() {
+ return pcd.values();
+ }
+
+ public ClassFileReader getClassFileReader() {
+ return cfr;
+ }
+
+ public ClassInfo getClassInfoForName(int verCode, String className) {
+ PCDEntry pcde = pcd.get(className);
+ if (pcde != null) {
+ return getClassInfoForPCDEntry(verCode, pcde);
+ } else {
+ return null;
+ }
+ }
+
+ public boolean isProjectClass(int verCode, String className) {
+ if (verCode == ClassInfo.VER_OLD) {
+ return pcd.containsKey(className);
+ } else {
+ PCDEntry pcde = pcd.get(className);
+ return (pcde != null && pcde.checkResult != PCDEntry.CV_DELETED);
+ }
+ }
+
+ /**
+ * Get an instance of ClassInfo (load a class file if necessary) for the given version (old or new) of
+ * the class determined by pcde. For an old class version, always returns a non-null result; but for a new
+ * version, null is returned if class file is not found. In most of the current uses of this method null result
+ * is not checked, because it's either called for an old version or it is already known that the .class file
+ * should be present; nevertheless, beware!
+ */
+ public ClassInfo getClassInfoForPCDEntry(int verCode, PCDEntry pcde) {
+ if (verCode == ClassInfo.VER_OLD) {
+ return pcde.oldClassInfo;
+ }
+
+ ClassInfo res = pcde.newClassInfo;
+ if (res == null) {
+ byte classFileBytes[];
+ String classFileFullPath = null;
+ if (pcde.javaFileFullPath.endsWith(".java")) {
+ File classFile = Utils.checkFileForName(pcde.classFileFullPath);
+ if (classFile == null) {
+ return null; // Class file not found.
+ }
+ classFileBytes = Utils.readFileIntoBuffer(classFile);
+ classFileFullPath = pcde.classFileFullPath;
+ } else {
+ try {
+ JarFile jarFile = new JarFile(pcde.javaFileFullPath);
+ JarEntry jarEntry =
+ jarFile.getJarEntry(pcde.className + ".class");
+ if (jarEntry == null) {
+ return null;
+ }
+ classFileBytes =
+ Utils.readZipEntryIntoBuffer(jarFile, jarEntry);
+ } catch (IOException ex) {
+ throw new PrivateException(ex);
+ }
+ }
+ res =
+ new ClassInfo(classFileBytes, verCode, this, classFileFullPath);
+ pcde.newClassInfo = res;
+ }
+ return res;
+ }
+
+ /**
+ * Returns null if class is compileable (has a .java source) and not recompiled yet, "" if
+ * class has already been recompiled or has been deleted from project, and the class's .jar
+ * name if class comes from a jar, hence is uncompileable.
+ */
+ public String classAlreadyRecompiledOrUncompileable(String className) {
+ PCDEntry pcde = pcd.get(className);
+ if (pcde == null) {
+ //!!!
+ for (String keyName : pcd.keySet()) {
+ PCDEntry entry = pcd.get(keyName);
+ if (entry.className.equals(className)) {
+ System.out.println("ERROR: inconsistent entry: key = " +
+ keyName + ", name in entry = " + entry.className);
+ }
+ }
+ //!!!
+ throw internalException(className + " not in project when it should be");
+ }
+ if (pcde.checkResult == PCDEntry.CV_DELETED) {
+ return "";
+ }
+ if (pcde.javaFileFullPath.endsWith(".jar")) {
+ return pcde.javaFileFullPath;
+ } else {
+ return (recompiledJavaFiles.contains(pcde.javaFileFullPath) ? "" : null);
+ }
+ }
+
+ /**
+ * Compiler initialization depends on compiler type specified.
+ * If jcExecApp != null, i.e. an external executable compiler application is used, and nothing has to be done.
+ * If externalApp != null, that is, jmake is called by an external application such as Ant, which
+ * manages compilation in its own way, and also nothing has to be done.
+ * Otherwise, load the compiler class and method (either specified through jcPath, jcMainClass and jcMethod,
+ * or the default one.
+ */
+ public void initializeCompiler(String jcExecApp,
+ String jcPath, String jcMainClass, String jcMethod,
+ Object externalApp, Method externalCompileSourceFilesMethod) {
+ ClassPath.initializeAllClassPaths();
+
+ if (externalApp != null) {
+ this.externalApp = externalApp;
+ this.externalCompileSourceFilesMethod =
+ externalCompileSourceFilesMethod;
+ return;
+ }
+ if (jcExecApp != null) {
+ this.jcExecApp = jcExecApp;
+ return;
+ }
+
+ if (jcPath == null) {
+ String javaHome = System.getProperty("java.home");
+ // In my tests it ends with '/jre'. Or it could be ending with '/bin' as well? Let's assume it can be both and delete
+ // this latter directory.
+ if (javaHome.endsWith(File.separator + "jre") || javaHome.endsWith(File.separator + "bin")) {
+ javaHome = javaHome.substring(0, javaHome.length() - 4);
+ }
+ jcPath = javaHome + "/lib/tools.jar";
+ }
+ ClassLoader compilerLoader;
+ try {
+ compilerLoader = ClassPath.getClassLoaderForPath(jcPath);
+ } catch (Exception ex) {
+ throw compilerInteractionException("error opening compiler path", ex, 0);
+ }
+
+ if (jcMainClass == null) {
+ jcMainClass = "com.sun.tools.javac.Main";
+ }
+ if (jcMethod == null) {
+ jcMethod = "compile";
+ }
+
+ try {
+ compilerClass = compilerLoader.loadClass(jcMainClass);
+ } catch (ClassNotFoundException e) {
+ throw compilerInteractionException("error loading compiler main class " + jcMainClass, e, 0);
+ }
+
+ Class<?>[] args = new Class<?>[]{String[].class};
+ try {
+ compileMethod = compilerClass.getMethod(jcMethod, args);
+ } catch (Exception e) {
+ throw compilerInteractionException("error getting method com.sun.tools.javac.Main.compile(String args[])", e, 0);
+ }
+ }
+
+ /** Main entrypoint for this class */
+ public void run() {
+ Utils.startTiming(Utils.TIMING_SYNCHRO);
+ synchronizeProjectFilesAndPCD();
+ Utils.stopAndPrintTiming("Synchro", Utils.TIMING_SYNCHRO);
+ Utils.printTiming("of which synchro check file", Utils.TIMING_SYNCHRO_CHECK_JAVA_FILES);
+
+ Utils.startTiming(Utils.TIMING_FIND_UPDATED_JAVA_FILES);
+ findUpdatedJavaAndJarFiles();
+ Utils.stopAndPrintTiming("findUpdatedJavaAndJarFiles", Utils.TIMING_FIND_UPDATED_JAVA_FILES);
+ Utils.printTiming("of which classFileObsoleteOrDeleted", Utils.TIMING_CLASS_FILE_OBSOLETE_OR_DELETED);
+
+ // Let's free some memory
+ projectJavaAndJarFilesArray = null;
+
+ updatedClasses = new LinkedHashSet<String>();
+ dealWithClassesInUpdatedJarFiles();
+
+ int iterNo = 0;
+ int res = 0;
+ while (iterNo == 0 || updatedJavaFiles.size() != 0 || newJavaFiles.size() != 0) {
+ // It may happen that we didn't find any updated or new .java files. However, we still need to enter
+ // this loop because there may be some class files that need compatibility checking. This can happen
+ // either if somebody had recompiled their sources bypassing jmake, or if their checking during the
+ // previous invocation of jmake failed, because their dependent code recompilation failed.
+ if (updatedJavaFiles.size() > 0 || newJavaFiles.size() > 0) {
+ Utils.startTiming(Utils.TIMING_COMPILE);
+ int intermediateRes = recompileUpdatedJavaFiles();
+ Utils.stopAndPrintTiming("Compile", Utils.TIMING_COMPILE);
+ if (intermediateRes != 0) {
+ res = intermediateRes;
+ }
+ }
+
+ Utils.startTiming(Utils.TIMING_PDBUPDATE);
+ // New classes can be added to pdb only if compilation was successful, i.e. the new project version is consistent.
+ if (iterNo++ == 0 && res == 0) {
+ findClassFilesForNewJavaAndJarFiles();
+ findClassFilesForUpdatedJavaFiles();
+ dealWithNestedClassesForUpdatedJavaFiles();
+ }
+ Utils.stopAndPrintTiming("Entering new classes in PDB", Utils.TIMING_PDBUPDATE);
+
+ updatedJavaFiles.clear();
+ newJavaFiles.clear();
+
+ Utils.startTiming(Utils.TIMING_FIND_UPDATED_CLASSES);
+ findUpdatedClasses();
+ Utils.stopAndPrintTiming("Find updated classes", Utils.TIMING_FIND_UPDATED_CLASSES);
+
+ Utils.startTiming(Utils.TIMING_CHECK_UPDATED_CLASSES);
+ checkDeletedClasses();
+ checkUpdatedClasses();
+ Utils.stopAndPrintTiming("Check updated classes", Utils.TIMING_CHECK_UPDATED_CLASSES);
+
+ updatedClasses = new LinkedHashSet<String>();
+ if (ClassPath.getVirtualPath() != null) {
+ if (res != 0)
+ break;
+ }
+ }
+
+ Utils.startTiming(Utils.TIMING_PDBWRITE);
+ updateClassFilesInfoInPCD(res);
+ pcdc.save();
+ Utils.stopAndPrintTiming("PDB write", Utils.TIMING_PDBWRITE);
+
+ if (res != 0) {
+ throw compilerInteractionException("compilation error(s)", null, res);
+ }
+ }
+
+ /**
+ * Find the newly-created class files for existing java files.
+ */
+ private void findClassFilesForUpdatedJavaFiles() {
+ if (dependencyFile == null)
+ return;
+
+ Set<String> allClasses = new HashSet<String>();
+
+ Map<String, List<String>> dependencies = parseDependencyFile();
+ for (String file : updatedJavaFiles) {
+ List<String> myDeps = dependencies.get(file);
+ if (myDeps != null) {
+ PCDEntry parent = getNamedPCDE(file, dependencies);
+ for (String dependency : myDeps) {
+ allClasses.add(dependency);
+ if (pcd.containsKey(dependency))
+ continue;
+ findClassFileOnFilesystem(file, parent, dependency, false);
+ }
+ }
+ }
+ for (Map.Entry<String, PCDEntry> entry : pcd.entrySet()) {
+ String cls = entry.getKey();
+ if (!allClasses.contains(cls)) {
+ PCDEntry pcde = entry.getValue();
+ if (updatedJavaFiles.contains(pcde.javaFileFullPath)) {
+ deletedClasses.add(cls);
+ }
+ }
+ }
+ }
+
+ public String[] getAllUpdatedClassesAsStringArray() {
+ String[] res = new String[allUpdatedClasses.size()];
+ int i = 0;
+ for (String updatedClass : allUpdatedClasses) {
+ res[i++] = updatedClass.replace('/', '.');
+ }
+ return res;
+ }
+
+ /**
+ * Synchronize projectJavaAndJarFilesArray and PCD, i.e. leave only those entries in the PCD which have their
+ * .java (.jar) files in projectJavaAndJarFilesArray. New .java files in projectJavaAndJarFilesArray (i.e. those
+ * for which there are no entries in the PCD yet) are added to newJavaFiles; new .jar files are added to newJarFiles.
+ * Alternatively, just use the supplied arrays of added and deleted .java and .jar files.
+ *
+ * For entries whose .java files are not in the PCD anymore, try to delete .class files. We need to do that before
+ * compilation to avoid the situation when a .java file is removed but compilation succeeds because the .class file
+ * is still there.
+ *
+ * Unfortunately, we also need to delete all class files for non-nested classes whose names differ from their .java
+ * file name, because we can't tell when they've been removed from their .java files -- but it's only safe to do this
+ * for files that originate from java files that we're compiling this round.
+ *
+ * Upon return from this method, all of the .java and .jar files in the PCD are known to exist.
+ */
+ private void synchronizeProjectFilesAndPCD() {
+ if (projectJavaAndJarFilesArray != null) {
+ Set<String> pcdJavaFilesSet = new LinkedHashSet<String>(pcd.size() * 3 / 2);
+ for(PCDEntry entry : entries()) {
+ pcdJavaFilesSet.add(entry.javaFileFullPath);
+ }
+
+ Set<String> canonicalPJF =
+ new LinkedHashSet<String>(projectJavaAndJarFilesArray.length * 3 / 2);
+
+ // Add .java files that are not in PCD to newJavaFiles; add .jar files that are not in PCD to newJarFiles.
+ for (int i = 0; i < projectJavaAndJarFilesArray.length; i++) {
+ String projFileName = projectJavaAndJarFilesArray[i];
+ Utils.startTiming(Utils.TIMING_SYNCHRO_CHECK_TMP);
+ File projFile = Utils.checkFileForName(projFileName);
+ Utils.stopAndAddTiming(Utils.TIMING_SYNCHRO_CHECK_TMP, Utils.TIMING_SYNCHRO_CHECK_JAVA_FILES);
+ if (projFile == null) {
+ throw new PrivateException(new FileNotFoundException("specified source file " + projFileName + " not found."));
+ }
+ // The main reason for using getAbsolutePath() instead of more reliable getCanonicalPath() is the fact that
+ // sometimes users may name the actual files containing Java code in some custom way, and give javac/jmake
+ // symbolic links to these files (that have correct .java names) instead. getCanonicalPath(), however, returns the
+ // real (i.e. user custom) file name, which will confuse our test below and then javac.
+ String absoluteProjFileName = projFile.getAbsolutePath();
+ // On Windows, make sure the drive letter is always in lower case
+ if (backSlashFileSeparator) {
+ absoluteProjFileName =
+ Utils.convertDriveLetterToLowerCase(absoluteProjFileName);
+ }
+ canonicalPJF.add(absoluteProjFileName);
+ if (!pcdJavaFilesSet.contains(absoluteProjFileName)) {
+ if (absoluteProjFileName.endsWith(".java")) {
+ newJavaFiles.add(absoluteProjFileName);
+ } else if (absoluteProjFileName.endsWith(".jar")) {
+ newJarFiles.add(absoluteProjFileName);
+ } else {
+ throw new PrivateException(new PublicExceptions.InvalidSourceFileExtensionException("specified source file " + projFileName + " has an invalid extension (not .java or .jar)."));
+ }
+ }
+ }
+
+ // Find the entries containing .java or .jar files that are not in project anymore
+ for (Entry<String, PCDEntry> entry : pcd.entrySet()) {
+ String key = entry.getKey();
+ PCDEntry e = entry.getValue();
+ e.oldClassInfo.restorePCDM(this);
+ if (canonicalPJF.contains(e.javaFileFullPath)) {
+ if (e.isPackagePrivateClass()) {
+ initializeClassFileFullPath(e);
+ new File(e.classFileFullPath).delete();
+ }
+ } else {
+ if (ClassPath.getVirtualPath() == null) {
+ deletedClasses.add(key);
+ } else {
+ // Okay, not found locally, but virtual path was defined, so try it now....
+ if ( (e.oldClassFileFingerprint == projectJavaAndJarFilesArray.length &&
+ newJavaFiles.size() == 0) ||
+ Utils.checkFileForName(e.javaFileFullPath) != null)
+ {
+ e.checkResult = PCDEntry.CV_NEWER_FOUND_NEARER;
+ e.oldClassFileFingerprint = projectJavaAndJarFilesArray.length;
+ }
+ else
+ {
+ String classFound = null;
+ String sourceFound = null;
+ // Find source and class file via virtual path
+ String path = ClassPath.getVirtualPath();
+ // TODO(Eric Ayers): IntelliJ static analysis shows several useless
+ // expressions that make this loop a no-op.
+ for (StringTokenizer st = new StringTokenizer(path, File.pathSeparator);
+ !(classFound != null && sourceFound != null) && st.hasMoreTokens();)
+ {
+ String fullPath = st.nextToken()+File.separator+e.className;
+ if (sourceFound != null && new File(fullPath+".java").exists())
+ {
+ sourceFound = fullPath + ".java";
+ }
+ if (classFound != null && new File(fullPath+".class").exists())
+ {
+ classFound = fullPath + ".class";
+ }
+ }
+ // TODO(Eric Ayers): IntelliJ static analysis shows that this expression
+ // is always true.
+ if (classFound == null)
+ {
+ deletedClasses.add(key);
+ if (e.javaFileFullPath.endsWith(".jar"))
+ {
+ deletedJarFiles.add(e.javaFileFullPath);
+ }
+ else
+ {
+ initializeClassFileFullPath(e);
+ (new File(e.classFileFullPath)).delete();
+ }
+ }
+ else if (sourceFound != null)
+ {
+ newJavaFiles.add(sourceFound);
+ e.checkResult = PCDEntry.CV_NEWER_FOUND_NEARER;
+ e.oldClassFileFingerprint = projectJavaAndJarFilesArray.length;
+ }
+ else
+ {
+ classFound = classFound.replace('/', File.separatorChar);
+ throw new PrivateException(new FileNotFoundException("deleted class " + classFound + " still exists."));
+ }
+ }
+ }
+ if (e.javaFileFullPath.endsWith(".jar")) {
+ deletedJarFiles.add(e.javaFileFullPath);
+ } else { // Try to delete a class file for the removed project class.
+ initializeClassFileFullPath(e);
+ (new File(e.classFileFullPath)).delete();
+ }
+ }
+ }
+ } else { // projectJavaAndJarFilesArray == null - use supplied arrays of added and removed .java and .jar files
+ if (addedJavaAndJarFilesArray != null) {
+ for (String fileName : addedJavaAndJarFilesArray) {
+ fileName = fileName.intern();
+ if (fileName.endsWith(".java")) {
+ newJavaFiles.add(fileName);
+ } else if (fileName.endsWith(".jar")) {
+ newJarFiles.add(fileName);
+ } else {
+ throw new PrivateException(new PublicExceptions.InvalidSourceFileExtensionException(
+ "specified source file " + fileName + " has an invalid extension (not .java or .jar)."));
+ }
+ }
+ }
+
+ Set<String> removedJavaAndJarFilesSet = null;
+ if (removedJavaAndJarFilesArray != null) {
+ removedJavaAndJarFilesSet = new LinkedHashSet<String>();
+ for (String fileName : removedJavaAndJarFilesArray) {
+ fileName = fileName.intern();
+ removedJavaAndJarFilesSet.add(fileName);
+ if (fileName.endsWith(".jar")) {
+ deletedJarFiles.add(fileName);
+ }
+ }
+ }
+
+ for (Entry<String, PCDEntry> entry : pcd.entrySet()) {
+ String key = entry.getKey();
+ PCDEntry e = entry.getValue();
+ e.oldClassInfo.restorePCDM(this);
+ if (removedJavaAndJarFilesSet != null &&
+ removedJavaAndJarFilesSet.contains(e.javaFileFullPath)) {
+ deletedClasses.add(key);
+ if (!e.javaFileFullPath.endsWith(".jar")) { // Try to delete a class file for the removed project class.
+ initializeClassFileFullPath(e);
+ (new File(e.classFileFullPath)).delete();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * In the end of run, update the information in the project database for the class files which have
+ * been updated and checked, or deleted. If compilationResult == 0, i.e. all recompilations were
+ * successful, information for new versions of all of the classes is made permanent, and entries
+ * for deleted classes are removed permanently. Otherwise, information is updated only for those
+ * classes whose old and new versions were found source compatible.
+ */
+ private void updateClassFilesInfoInPCD(int compilationResult) {
+ // If the project appears to be inconsistent after changes, make a preliminary pass that will deal with enclosing
+ // classes for deleted nested classes. The problem with them can be as follows: we delete a nested class C$X,
+ // which is still referenced from somewhere. However, C has not changed at all or at least incompatibly, and
+ // thus we update its PCDEntry, which now does not reference C$X. Other parts of jmake require that a nested
+ // class is always referenced from its directly enclusing class, thus to keep the PCD consistent we have to remove
+ // C$X from the PCD. On the next invocation of jmake, C$X is not in the PDB at all, and thus any classes that
+ // may still reference it and have not been updated are not checked => project becomes inconsistent. We could do
+ // better by immediately marking enclosing classes incompatible once we detect that a deleted nested class is
+ // really referenced from somewhere, but the solution below seems to be more robust.
+ if (compilationResult != 0) {
+ for (String className : updatedAndCheckedClasses) {
+ PCDEntry entry = pcd.get(className);
+ if (entry.checkResult == PCDEntry.CV_DELETED &&
+ !"".equals(entry.oldClassInfo.directlyEnclosingClass)) {
+ PCDEntry enclEntry =
+ pcd.get(entry.oldClassInfo.directlyEnclosingClass);
+ enclEntry.checkResult = PCDEntry.CV_INCOMPATIBLE;
+ }
+ }
+ }
+
+ for (String className : updatedAndCheckedClasses) {
+ PCDEntry entry = pcd.get(className);
+ if (entry.checkResult == PCDEntry.CV_UNCHECKED) {
+ continue;
+ }
+ if (ClassPath.getVirtualPath() != null) {
+ if (entry.checkResult == PCDEntry.CV_NEWER_FOUND_NEARER) {
+ continue;
+ }
+ }
+ if (entry.checkResult == PCDEntry.CV_DELETED) {
+ if (compilationResult == 0) {
+ pcd.remove(className); // Only if consistency checking is ok, a deleted class can be safely removed from the PCD
+ }
+ } else if (entry.checkResult == PCDEntry.CV_COMPATIBLE ||
+ entry.checkResult == PCDEntry.CV_NEW ||
+ (entry.checkResult == PCDEntry.CV_INCOMPATIBLE && compilationResult == 0)) {
+ if (entry.newClassInfo == null) { // "Safety net" for the (hopefully unlikely) case we overlooked something before...
+ Utils.printWarningMessage("Warning: internal information inconsistency detected during pdb updating");
+ Utils.printWarningMessage(Utils.REPORT_PROBLEM);
+ Utils.printWarningMessage("Class name: " + className);
+ if (entry.checkResult == PCDEntry.CV_NEW) {
+ pcd.remove(className);
+ } else {
+ continue;
+ }
+ }
+ entry.oldClassFileLastModified = entry.newClassFileLastModified;
+ entry.oldClassFileFingerprint = entry.newClassFileFingerprint;
+ entry.oldClassInfo = entry.newClassInfo;
+ }
+ }
+ }
+
+ /**
+ * Find all .java files on the filesystem, for which the .class file does not exist
+ * or is newer than the .java file. Also find all .jar files for which the timestamp
+ * has changed. Alternatively, just use the supplied array of updated .java/.jar files.
+ */
+ private void findUpdatedJavaAndJarFiles() {
+ boolean projectSpecifiedAsAllSources =
+ projectJavaAndJarFilesArray != null;
+ for (PCDEntry entry : entries()) {
+ if (deletedClasses.contains(entry.className)) {
+ continue;
+ }
+ if (entry.javaFileFullPath.endsWith(".java")) {
+ initializeClassFileFullPath(entry);
+ if (projectSpecifiedAsAllSources) {
+ if (ClassPath.getVirtualPath() != null) {
+ String paths[] = ClassPath.getVirtualPath().split(File.pathSeparator);
+ String tmpClassName = entry.className;
+ tmpClassName = tmpClassName.replaceAll("\\Q$\\E.*$", "");
+ for (int i=0; i<paths.length; i++) {
+ String tmpFilename = paths[i] + File.separator + tmpClassName + ".java";
+ File tmpFile = new File(tmpFilename);
+ if (tmpFile.exists()) {
+ entry.javaFileFullPath = tmpFile.getAbsolutePath();
+ break;
+ }
+ }
+ }
+ Utils.startTiming(Utils.TIMING_CLASS_FILE_OBSOLETE_TMP);
+ if (classFileObsoleteOrDeleted(entry)) {
+ updatedJavaFiles.add(entry.javaFileFullPath);
+ }
+ Utils.stopAndAddTiming(Utils.TIMING_CLASS_FILE_OBSOLETE_TMP, Utils.TIMING_CLASS_FILE_OBSOLETE_OR_DELETED);
+ }
+ entry.checked = true;
+ } else { // Class coming from a .jar file. Mark this entry as checked only if its JAR hasn't changed
+ if (projectJavaAndJarFilesArray != null) {
+ entry.checked = !checkJarFileForUpdate(entry);
+ }
+ }
+ }
+
+ // Lists of updated/added/deleted source files specified instead of a full list of project sources
+ if (!projectSpecifiedAsAllSources && updatedJavaAndJarFilesArray != null) {
+ for (int i = 0; i < updatedJavaAndJarFilesArray.length; i++) {
+ if (updatedJavaAndJarFilesArray[i].endsWith(".java")) {
+ updatedJavaFiles.add(updatedJavaAndJarFilesArray[i]);
+ } else {
+ updatedJarFiles.add(updatedJavaAndJarFilesArray[i]);
+ }
+ }
+ }
+ }
+
+ private boolean classFileObsoleteOrDeleted(PCDEntry entry) {
+ if (ClassPath.getVirtualPath() != null) {
+ File file1 = new File(entry.javaFileFullPath);
+ if (!file1.exists())
+ throw new PrivateException(new FileNotFoundException("specified source file " +
+ entry.javaFileFullPath + " not found."));
+ if (file1.lastModified() < entry.oldClassFileLastModified)
+ {
+ return false;
+ }
+ }
+ File classFile = Utils.checkFileForName(entry.classFileFullPath);
+ if (classFile == null || !classFile.exists()) {
+ return true; // Class file has been deleted
+ }
+ File javaFile = new File(entry.javaFileFullPath); // Guaranteed to exist at this point
+ if (classFile.lastModified() <= javaFile.lastModified()) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean checkJarFileForUpdate(PCDEntry entry) {
+ String jarFileName = entry.javaFileFullPath;
+ if (stableJarFiles.contains(jarFileName)) {
+ return false;
+ } else if (updatedJarFiles.contains(jarFileName) ||
+ newJarFiles.contains(jarFileName) ||
+ deletedJarFiles.contains(jarFileName)) {
+ return true;
+ } else {
+ File jarFile = new File(jarFileName); // Guaranteed to exist at this point.
+ if (entry.oldClassFileLastModified != jarFile.lastModified()) {
+ updatedJarFiles.add(jarFileName);
+ return true;
+ } else {
+ stableJarFiles.add(jarFileName);
+ return false;
+ }
+ }
+ }
+
+ public int recompileUpdatedJavaFiles() {
+ if (externalApp != null) {
+ return recompileUpdatedJavaFilesUsingExternalMethod();
+ } else {
+ return recompileUpdatedJavaFilesOurselves();
+ }
+ }
+
+ private int recompileUpdatedJavaFilesOurselves() {
+ int filesNo = updatedJavaFiles.size() + newJavaFiles.size();
+ int addArgsNo = javacAddArgs.size();
+ int argsNo = addArgsNo + filesNo + 2;
+ String compilerBootClassPath, compilerExtDirs;
+ if ((compilerBootClassPath = ClassPath.getCompilerBootClassPath()) != null) {
+ argsNo += 2;
+ }
+ if ((compilerExtDirs = ClassPath.getCompilerExtDirs()) != null) {
+ argsNo += 2;
+ }
+ if (jcExecApp != null) {
+ argsNo++;
+ }
+ String args[] = new String[argsNo];
+ int pos = 0;
+ if (jcExecApp != null) {
+ args[pos++] = jcExecApp;
+ }
+ for (int i = 0; i < addArgsNo; i++) {
+ args[pos++] = javacAddArgs.get(i);
+ }
+ args[pos++] = "-classpath";
+ args[pos++] = ClassPath.getCompilerUserClassPath();
+ if (compilerBootClassPath != null) {
+ args[pos++] = "-bootclasspath";
+ args[pos++] = compilerBootClassPath;
+ }
+ if (compilerExtDirs != null) {
+ args[pos++] = "-extdirs";
+ args[pos++] = compilerExtDirs;
+ }
+ if (!newProject) {
+ Utils.printInfoMessage("Recompiling source files:");
+ }
+ for (String javaFileFullPath : updatedJavaFiles) {
+ if (!newProject) {
+ Utils.printInfoMessage(javaFileFullPath);
+ }
+ recompiledJavaFiles.add(args[pos++] = javaFileFullPath);
+ }
+ for (int j = 0; j < newJavaFiles.size(); j++) {
+ String javaFileFullPath = newJavaFiles.get(j);
+ if (!newProject) {
+ Utils.printInfoMessage(javaFileFullPath);
+ }
+ recompiledJavaFiles.add(args[pos++] = javaFileFullPath);
+ }
+
+ if (jcExecApp == null) { // Executing javac or some other compiler within the same JVM
+ Object reflectArgs[] = new Object[1];
+ reflectArgs[0] = args;
+ try {
+ Object dummy = compilerClass.newInstance();
+ Integer res = (Integer) compileMethod.invoke(dummy, reflectArgs);
+ return res.intValue();
+ } catch (Exception e) {
+ throw compilerInteractionException("exception thrown when trying to invoke the compiler method", e, 0);
+ }
+ } else { // Executing an external Java compiler, such as jikes
+ int exitCode = 0;
+ try {
+ Process p = Runtime.getRuntime().exec(args);
+ InputStream pErr = p.getErrorStream();
+ InputStream pOut = p.getInputStream();
+ boolean terminated = false;
+
+ while (!terminated) {
+ try {
+ exitCode = p.exitValue();
+ terminated = true;
+ } catch (IllegalThreadStateException itse) { // Process not yet terminated, wait for some time
+ Utils.ignore(itse);
+ Utils.delay(100);
+ }
+ try {
+ Utils.readAndPrintBytesFromStream(pErr, System.err);
+ Utils.readAndPrintBytesFromStream(pOut, System.out);
+ } catch (IOException ioe1) {
+ throw compilerInteractionException("I/O error when reading the compiler application output", ioe1, exitCode);
+ }
+ }
+ return exitCode;
+ } catch (IOException ioe2) {
+ throw compilerInteractionException("I/O error when trying to invoke the compiler application", ioe2, exitCode);
+ }
+ }
+ }
+
+ /** Execution under complete control of external app - use externally supplied method to recompile classes */
+ private int recompileUpdatedJavaFilesUsingExternalMethod() {
+ int filesNo = updatedJavaFiles.size() + newJavaFiles.size();
+ String[] fileNames = new String[filesNo];
+ int i = 0;
+ for (String updatedFile : updatedJavaFiles) {
+ recompiledJavaFiles.add(fileNames[i] = updatedFile);
+ }
+ for (int j = 0; j < newJavaFiles.size(); j++) {
+ recompiledJavaFiles.add(fileNames[i++] = newJavaFiles.get(j));
+ }
+
+ try {
+ Integer res =
+ (Integer) externalCompileSourceFilesMethod.invoke(externalApp, new Object[]{fileNames});
+ return res.intValue();
+ } catch (IllegalAccessException e1) {
+ throw compilerInteractionException("compiler method is not accessible", e1, 0);
+ } catch (IllegalArgumentException e2) {
+ throw compilerInteractionException("illegal arguments passed to compiler method", e2, 0);
+ } catch (InvocationTargetException e3) {
+ throw compilerInteractionException("exception when executing the compiler method", e3, 0);
+ }
+ }
+
+ /**
+ * For each .java file from newJavaFiles, find all of the .class files, the names of which we can
+ * logically deduce (a top-level class with the same name, and all of the nested classes),
+ * and put the info on them into the PCD. Also include any class files from the dependencyFile,
+ * if any. For each .jar file from newJarFiles, find all of the .class files in that archive and
+ * put info on them into the PCD.
+ */
+ private void findClassFilesForNewJavaAndJarFiles() {
+ for (String javaFileFullPath : newJavaFiles) {
+ PCDEntry pcde =
+ findClassFileOnFilesystem(javaFileFullPath, null, null, false);
+
+ if (pcde == null) {
+ // .class file not found - possible compilation error
+ if (missingClassIsOk(javaFileFullPath)) {
+ continue;
+ } else {
+ throw new PrivateException(new PublicExceptions.ClassNameMismatchException(
+ "Could not find class file for " + javaFileFullPath));
+ }
+ }
+ Set<String> entries = new HashSet<String>();
+ if (pcde.checkResult == PCDEntry.CV_NEW) { // It's really a new .java file, not a moved one
+ entries.addAll(findAndUpdateAllNestedClassesForClass(pcde, false));
+ } else {
+ entries.addAll(findAndUpdateAllNestedClassesForClass(pcde, true));
+ }
+ entries.add(pcde.className);
+ if (dependencyFile != null) {
+ Map<String, List<String>> dependencies = parseDependencyFile();
+ List<String> myDeps = dependencies.get(javaFileFullPath);
+ if (myDeps != null) {
+ for (String dependency : myDeps) {
+ if (entries.contains(dependency))
+ continue;
+ findClassFileOnFilesystem(javaFileFullPath, pcde,
+ dependency, false);
+ }
+ }
+ }
+ }
+
+ for (String newJarFile : newJarFiles) {
+ processAllClassesFromJarFile(newJarFile);
+ }
+ }
+
+ /**
+ * Parse an extra dependency file. The format of the file is a series of lines,
+ * each consisting of:
+ * SourceFileName.java -> ClassName
+ * (these file names are relative to destDir)
+ */
+ private Map<String, List<String>> parseDependencyFile() {
+ if (!destDirSpecified)
+ throw new RuntimeException("Dependency files require destDir");
+ if (extraDependencies != null)
+ return extraDependencies;
+ BufferedReader in = null;
+ try {
+ extraDependencies = new HashMap<String, List<String>>();
+ in = new BufferedReader(new FileReader(dependencyFile));
+ int lineNumber = 0;
+ while (true) {
+ lineNumber ++;
+ String line = in.readLine();
+ if (line == null)
+ break;
+ String[] parts = line.split("->");
+ if (parts.length != 2) {
+ throw new RuntimeException("Failed to parse line " + lineNumber + " of " + dependencyFile
+ + ". Expected {foo.java} -> {classname}.");
+ }
+ String src = parts[0].trim();
+ src = new File(destDir, src).getCanonicalPath();
+ String cls = parts[1].trim();
+ List<String> classes = extraDependencies.get(src);
+ if (classes == null) {
+ classes = new ArrayList<String>();
+ extraDependencies.put(src, classes);
+ }
+ cls = cls.substring(0, cls.length() - 6); // strip trailing ".class"
+ classes.add(cls);
+ }
+ } catch (IOException e) {
+ throw new PrivateException(e);
+ } finally {
+ if (in != null)
+ try {
+ in.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return extraDependencies;
+ }
+
+ /**
+ * In most cases we want to fail the build if a class cannot be found.
+ *
+ * However there is one common valid case where a .java file might not contain
+ * a class: package-info.java files.
+ *
+ * See this doc for more info: http://docs.oracle.com/javase/specs/jls/se7/html/jls-7.html
+ */
+ private boolean missingClassIsOk(String javaFileFullPath) {
+ return javaFileFullPath != null && "package-info.java".equals(new File(javaFileFullPath).getName());
+ }
+
+ /**
+ * Find the .class file for the given javaFileFullPath and create a new PCDEntry for it.
+ * If enclosingClassPCDE is null, the named top-level class for the given .java file is looked up.
+ * Otherwise, the specified class specified by nestedClassFullName is looked up.
+ */
+ private PCDEntry findClassFileOnFilesystem(String javaFileFullPath, PCDEntry enclosingClassPCDE, String nestedClassFullName, boolean isNested) {
+ String classFileFullPath = null;
+ String fullClassName;
+ File classFile = null;
+
+ if (enclosingClassPCDE == null) { // Looking for a top-level class. May need to locate an appropriate directory.
+ // Remove the ".java" suffix. A Windows disk-name prefix, such as 'c:', will be cut off later automatically
+ fullClassName =
+ javaFileFullPath.substring(0, javaFileFullPath.length() - 5);
+ if (destDirSpecified) {
+ // Search for the .class file. We first assume the longest possible name. In case of failure,
+ // we cut the assumed top-most package from it and repeat the search.
+ while (classFile == null) {
+ classFileFullPath = destDir + fullClassName + ".class";
+ classFile = Utils.checkFileForName(classFileFullPath);
+ if (classFile == null) {
+ int cutIndex = fullClassName.indexOf(File.separatorChar);
+ if (cutIndex == -1) {
+ // Most probably, there was an error during compilation of this file.
+ // This does not prevent us from continuing.
+ Utils.printWarningMessage("Warning: unable to find .class file corresponding to source " + javaFileFullPath + ": expected " + classFileFullPath);
+
+ return null;
+ }
+ fullClassName = fullClassName.substring(cutIndex + 1);
+ }
+ }
+ } else {
+ classFileFullPath = fullClassName + ".class";
+ classFile = Utils.checkFileForName(classFileFullPath);
+ if (classFile == null) {
+ Utils.printWarningMessage("Warning: unable to find .class file corresponding to source " + javaFileFullPath);
+ return null;
+ }
+ }
+ } else { // Looking for a nested class, which always sits in the same directory as its enclosing class
+ classFileFullPath =
+ Utils.getClassFileFullPathForNestedClass(enclosingClassPCDE.classFileFullPath, nestedClassFullName);
+ classFile = Utils.checkFileForName(classFileFullPath);
+ if (classFile == null) {
+ Utils.printWarningMessage("Warning: unable to find .class file corresponding to nested class " + nestedClassFullName);
+ return null;
+ }
+ fullClassName = nestedClassFullName;
+ }
+
+ if (backSlashFileSeparator) {
+ fullClassName = fullClassName.replace(File.separatorChar, '/');
+ }
+
+ byte classFileBytes[] = Utils.readFileIntoBuffer(classFile);
+ ClassInfo classInfo =
+ new ClassInfo(classFileBytes, ClassInfo.VER_NEW, this, classFileFullPath);
+ if (isNested) {
+ if (!classInfo.directlyEnclosingClass.equals(enclosingClassPCDE.newClassInfo.name)) {
+ // Check if the above strings are like A and A$1. If so, there is actually no problem - the correct
+ // answer is A$1. The reason why just A was determined as a directly enclosing class when parsing
+ // class classInfo is due to the ambiguous interpretation of names like A$1$B. Such a name may mean
+ // (1) a non-member local nested class B of A, or (2) a member class B of an anonymous nested class A$1.
+ // When parsing any non-toplevel class, the first interpretation is always used.
+ // NOTE FOR JDK 1.5 - starting from this version, there is no ambiguity anymore.
+ // (1) will be called A$1B, and (2) will still be A$1$B
+ String a = classInfo.directlyEnclosingClass;
+ String ad1 = enclosingClassPCDE.newClassInfo.name;
+ if (!((classInfo.javacTargetRelease == Utils.JAVAC_TARGET_RELEASE_OLDEST) &&
+ (ad1.startsWith(a + "$") && Character.isDigit(ad1.charAt(a.length() + 1))))) {
+ throw new PrivateException(new PublicExceptions.ClassFileParseException(
+ "Enclosing class names for class " + classInfo.name + " don't match:\n" +
+ classInfo.directlyEnclosingClass + " and " + enclosingClassPCDE.newClassInfo.name));
+ }
+ }
+ }
+
+ // If dest dir was specified, check if the deduced name is equal to the one in this class (in this case
+ // they should necessarily match). Otherwise, without parsing the .java file, we can't reliably say what the
+ // full class name (actually, its package part) should be - so we just note the name.
+ if (destDirSpecified) {
+ if (!fullClassName.equals(classInfo.name)) {
+ throw new PrivateException(new PublicExceptions.ClassNameMismatchException(
+ "Error: deduced class name is different from the real one for source " +
+ javaFileFullPath + "\n" + fullClassName + " and " + classInfo.name));
+ }
+ } else {
+ fullClassName = classInfo.name;
+ }
+
+ if (enclosingClassPCDE != null) {
+ javaFileFullPath = enclosingClassPCDE.javaFileFullPath;
+ }
+ long classFileLastMod = classFile.lastModified();
+ long classFileFP = computeFP(classFileBytes);
+
+ if (pcd.containsKey(fullClassName)) {
+ PCDEntry pcde = pcd.get(fullClassName);
+ // If this entry has already been checked, it's a second entry for the same class, which is illegal.
+ if (pcde.checkResult == PCDEntry.CV_NEWER_FOUND_NEARER) {
+ // Newer copy of same file found in closer layer
+ // Reset to CV_UNCHECKED and skip redundnacy check
+ // as we know this would be redundant
+ pcde.checkResult = PCDEntry.CV_UNCHECKED;
+ } else {
+ if (pcde.checked) {
+ throw new PrivateException(new PublicExceptions.DoubleEntryException(
+ "Two entries for class " + classInfo.name + " detected: " + pcde.javaFileFullPath + " and " + javaFileFullPath));
+ }
+ }
+ // Otherwise, it means that the .java file for this class has been moved. jmake initially interprets
+ // a new source file name as a new class, and it's only at this point that we can actually see that it was
+ // only a move. We update javaFileFullPath for nested classes after we return from here.
+ pcde.javaFileFullPath = javaFileFullPath;
+ pcde.classFileFullPath = classFileFullPath;
+ pcde.newClassInfo = classInfo;
+ if (deletedClasses.contains(fullClassName)) {
+ deletedClasses.remove(fullClassName);
+ }
+ return pcde;
+ }
+
+ PCDEntry pcde = new PCDEntry(fullClassName,
+ javaFileFullPath,
+ classFileFullPath, classFileLastMod, classFileFP,
+ classInfo);
+ pcde.checkResult = PCDEntry.CV_NEW; // So that later it's promoted into oldClassInfo correctly
+ updatedAndCheckedClasses.add(fullClassName); // So that the above happens
+ pcd.put(fullClassName, pcde);
+ return pcde;
+ }
+
+ /**
+ * For the given class, find all direct nested classes (which may include reading their .class files from the
+ * class path) and set their access flags (contained in this, enclosing class, object) appropriately. If
+ * this class is a one coming from a .java source, repeat the procedure for each nested class in turn.
+ * Otherwise, i.e. if a class comes from a .jar, don't bother, since we will come across each of these
+ * classes anyway - when scanning their .jar. If 'move' parameter is true, it means that this method is called for
+ * a class that is not new, but has been moved (and possibly updated).
+ */
+ private Set<String> findAndUpdateAllNestedClassesForClass(PCDEntry pcde, boolean move) {
+ ClassInfo classInfo = pcde.newClassInfo;
+ if (classInfo.nestedClasses == null) {
+ return Collections.emptySet();
+ }
+ Set<String> entries = new LinkedHashSet<String>();
+ String nestedClasses[] = classInfo.nestedClasses;
+ String javaFileFullPath = pcde.javaFileFullPath;
+ String enclosingClassFileFullPath = pcde.classFileFullPath;
+ boolean isJavaSourceFile = javaFileFullPath.endsWith(".java");
+
+ for (int i = 0; i < nestedClasses.length; i++) {
+ PCDEntry nestedPCDE = pcd.get(nestedClasses[i]);
+ if (nestedPCDE == null) {
+ if (isJavaSourceFile) {
+ nestedPCDE =
+ findClassFileOnFilesystem(null, pcde, nestedClasses[i], true);
+ }
+ // For classes that come from a .jar, pcde should already be there. Otherwise this class just doesn't exist.
+ if (nestedPCDE == null) {
+ // Probably a compilation error, such that enclosing class is compiled but nested is not.
+ throw new PrivateException(new PublicExceptions.ClassNameMismatchException(
+ "Could not find class file for " + pcde.toString()));
+ }
+ }
+ if (move) {
+ if (deletedClasses.contains(nestedClasses[i])) {
+ deletedClasses.remove(nestedClasses[i]);
+ }
+ nestedPCDE.javaFileFullPath = javaFileFullPath;
+ if (javaFileFullPath.endsWith(".java")) {
+ nestedPCDE.classFileFullPath =
+ Utils.getClassFileFullPathForNestedClass(enclosingClassFileFullPath, nestedClasses[i]);
+ } else {
+ nestedPCDE.classFileFullPath = javaFileFullPath;
+ }
+ }
+ if (nestedPCDE.newClassInfo == null) {
+ getClassInfoForPCDEntry(ClassInfo.VER_NEW, nestedPCDE);
+ }
+ nestedPCDE.newClassInfo.accessFlags =
+ pcde.newClassInfo.nestedClassAccessFlags[i];
+ nestedPCDE.newClassInfo.isNonMemberNestedClass =
+ pcde.newClassInfo.nestedClassNonMember[i];
+
+ entries.add(nestedPCDE.className);
+ entries.addAll(findAndUpdateAllNestedClassesForClass(nestedPCDE, move));
+ }
+ return entries;
+ }
+
+ /**
+ * Take care of new nested classes that could have been generated from already existing .java sources,
+ * and of nested classes that do not exist anymore because they were deleted from these sources.
+ */
+ private void dealWithNestedClassesForUpdatedJavaFiles() {
+ if (updatedJavaFiles.size() == 0) {
+ return;
+ }
+
+ // First put PCDEntries for all updated classes that have nested classes into a temporary list.
+ // That's because we can then find new nested classes, which we will need to add to the PCD, which
+ // may probably conflict with us still iterating over it.
+ List<PCDEntry> updatedEntries = new ArrayList<PCDEntry>();
+ for (PCDEntry pcde : entries()) {
+ if (pcde.checkResult == PCDEntry.CV_NEW) {
+ continue; // This class has just been added to the PCD
+ }
+ if (updatedJavaFiles.contains(pcde.javaFileFullPath)) {
+ ClassInfo oldClassInfo = pcde.oldClassInfo;
+ ClassInfo newClassInfo =
+ getClassInfoForPCDEntry(ClassInfo.VER_NEW, pcde);
+ if (newClassInfo == null) {
+ deletedClasses.add(pcde.className);
+ continue; // Class file deleted then not re-created due to a compilation error somewhere.
+ }
+ if (oldClassInfo.nestedClasses != null || newClassInfo.nestedClasses != null) {
+ updatedEntries.add(pcde);
+ }
+ }
+ }
+
+ if (dependencyFile != null) {
+ Map<String, List<String>> dependencies = parseDependencyFile();
+ for (String file : updatedJavaFiles) {
+ List<String> myDeps = dependencies.get(file);
+ if (myDeps == null)
+ continue;
+ PCDEntry pcde = getNamedPCDE(file, dependencies);
+ for (String dependency : myDeps) {
+ PCDEntry dep = pcd.get(dependency);
+ if (dep != null)
+ // This is an existing dep.
+ continue;
+ dep = findClassFileOnFilesystem(file, pcde, dependency, false);
+ getClassInfoForPCDEntry(ClassInfo.VER_NEW, dep);
+ if (dep.newClassInfo.nestedClasses != null)
+ updatedEntries.add(dep);
+ }
+ }
+ }
+ dealWithNestedClassesForUpdatedPCDEntries(updatedEntries, false);
+ }
+
+ private PCDEntry getNamedPCDE(String file, Map<String, List<String>> dependencies) {
+ List<String> depsForFile = dependencies.get(file);
+ PCDEntry pcde = null;
+ // Find a non-nested class for this java file for which we already have
+ // a pcde
+ for (String dependency : depsForFile) {
+ if (dependency.indexOf('$') != -1)
+ continue;
+ pcde = pcd.get(dependency);
+ if (pcde != null)
+ break;
+ }
+ if (pcde == null) {
+ throw new PrivateException(new PublicExceptions.InternalException(file
+ + " was supposed to be an updated file, but there are no PCDEntries for any of its deps"));
+ }
+ return pcde;
+ }
+
+ private void dealWithNestedClassesForUpdatedPCDEntries(List<PCDEntry> entries, boolean move) {
+ for (int i = 0; i < entries.size(); i++) {
+ PCDEntry pcde = entries.get(i);
+ ClassInfo oldClassInfo = pcde.oldClassInfo;
+ ClassInfo newClassInfo = pcde.newClassInfo;
+ if (newClassInfo.nestedClasses != null) {
+ Set<String> nested = findAndUpdateAllNestedClassesForClass(pcde, move);
+ if (oldClassInfo.nestedClasses != null) { // Check if any old nested classes don't exist anymore
+ for (int j = 0; j < oldClassInfo.nestedClasses.length; j++) {
+ boolean found = false;
+ String oldNestedClass = oldClassInfo.nestedClasses[j];
+ for (int k = 0; k < newClassInfo.nestedClasses.length; k++) {
+ if (oldNestedClass.equals(newClassInfo.nestedClasses[k])) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ deletedClasses.add(oldNestedClass);
+ }
+ }
+ }
+ } else { // newNestedClasses == null and oldNestedClasses != null, so all nested classes have been removed in the new version
+ for (int j = 0; j < oldClassInfo.nestedClasses.length; j++) {
+ deletedClasses.add(oldClassInfo.nestedClasses[j]);
+ }
+ }
+ }
+ }
+
+ private void findUpdatedClasses() {
+ // This (iterating over all of the classes once again after performing that in classFileObsoleteOrDeleted()) may
+ // seem time-consuming, but in reality it isn't, since the most time-consuming operation of obtaining internal
+ // file handles for class files has already been performed in classFileObsoleteOrDeleted(). Once we have done that,
+ // this re-iteration takes very small amount of time. However, if we switch from "class file older than .java
+ // file" to ".java file timestamp changed" condition for recompilation, this will have to be changed as well.
+ for (PCDEntry entry : entries()) {
+ String className = entry.className;
+ if (updatedAndCheckedClasses.contains(className) ||
+ deletedClasses.contains(className)) {
+ continue;
+ }
+ if (!entry.javaFileFullPath.endsWith(".java")) {
+ continue; // classes from (updated) .jars have been dealt with separately
+ }
+ //DAB TODO understand this bit better. It is needed to support -vpath, I'm just not sure why....
+ if (entry.checkResult != PCDEntry.CV_NEWER_FOUND_NEARER &&
+ !updatedAndCheckedClasses.contains(className) &&
+ !deletedClasses.contains(className) &&
+ entry.javaFileFullPath.endsWith(".java") &&
+ classFileUpdated(entry))
+ {
+ //DAB TODO this is the old way....
+ //DAB if (classFileUpdated(entry)) {
+ updatedClasses.add(className);
+ allUpdatedClasses.add(className);
+ }
+ }
+ }
+
+ private boolean classFileUpdated(PCDEntry entry) {
+ File classFile = Utils.checkFileForName(entry.classFileFullPath);
+ if (classFile == null) {
+ return false;
+ }
+ // The only case when the above can happen is if class file was first deleted, and then there
+ // was an error recompiling its source
+
+ long classFileLastMod = classFile.lastModified();
+
+ if (classFileLastMod > entry.oldClassFileLastModified) {
+ entry.newClassFileLastModified = classFileLastMod;
+ // Check if the class was actually modified, to avoid the costly procedure of detailed version compare
+ long classFileFP = computeFP(classFile);
+ if (classFileFP != entry.oldClassFileFingerprint) {
+ entry.newClassFileFingerprint = classFileFP;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Compare old (preserved in pdb) and new (file system) versions of updated classes, and find all
+ * potentially affected dependent classes.
+ */
+ private void checkUpdatedClasses() {
+ for (String className : updatedClasses) {
+ PCDEntry pcde = pcd.get(className);
+ getClassInfoForPCDEntry(ClassInfo.VER_NEW, pcde);
+ if (!"".equals(pcde.oldClassInfo.directlyEnclosingClass)) {
+ // The following problem can occur with nested classes. A C.java source has been changed, so that C.class is
+ // not changed or changed in a compatible way, whereas the access modifiers of C$X.class are changed in an
+ // incompatible way, so that something is broken in the project. When jmake is called for the first time,
+ // it reports the problem, then saves the info on the new version of C in the pdb. Of course, the record for
+ // C$X in the pdb is not updated, since the change to it is incompatible and recompilation of dependent sources
+ // has failed. Suppose we don't change anything and invoke jmake again. C$X is found different from its old
+ // version and is checked here again. The outcome should be the same. But since C has not changed, C.class is
+ // not read from disk and the access flags of C$X, which are stored in C.class, are not set appropriately. So
+ // in such circumstances we have wrong access flags for C$X here. To fix the problem we need to load C explicitly.
+ ClassInfo enclosingClassInfo =
+ getClassInfoForName(ClassInfo.VER_NEW, pcde.oldClassInfo.directlyEnclosingClass);
+ //if (enclosingClassInfo == null || enclosingClassInfo.nestedClasses == null) {
+ // System.out.println("!!! Suspicious updated class name = " + className);
+ // System.out.println("!!! enclosingClassInfo for it = " + enclosingClassInfo);
+ // if (enclosingClassInfo != null) {
+ // System.out.println("!!! enclosingClassInfo.name = " + enclosingClassInfo.name);
+ // if (enclosingClassInfo.nestedClasses == null) System.out.println("!!! enclosingClassInfo.nestedClasses = null");
+ // }
+ //}
+ if (enclosingClassInfo.nestedClasses != null) { // Can be that this nested class was the only one for enclosing class, and it's deleted now
+ for (int i = 0; i < enclosingClassInfo.nestedClasses.length; i++) {
+ if (className.equals(enclosingClassInfo.nestedClasses[i])) {
+ pcde.newClassInfo.accessFlags =
+ enclosingClassInfo.nestedClassAccessFlags[i];
+ pcde.newClassInfo.isNonMemberNestedClass =
+ enclosingClassInfo.nestedClassNonMember[i];
+ break;
+ }
+ }
+ }
+ }
+ if (!(pcde.oldClassInfo.isNonMemberNestedClass && pcde.newClassInfo.isNonMemberNestedClass)) {
+ Utils.printInfoMessage("Checking " + pcde.className);
+ pcde.checkResult = cv.compareClassVersions(pcde) ? PCDEntry.CV_COMPATIBLE
+ : PCDEntry.CV_INCOMPATIBLE;
+ String affectedClasses[] = cv.getAffectedClasses();
+ if (affectedClasses != null) {
+ for (int i = 0; i < affectedClasses.length; i++) {
+ PCDEntry affEntry = pcd.get(affectedClasses[i]);
+ updatedJavaFiles.add(affEntry.javaFileFullPath);
+ }
+ }
+ } else {
+ // A non-member nested class can not be referenced by the source code of any class defined outside the
+ // immediately enclosing source code block for this class. Therefore, any incompatibility in the new
+ // version of this class can affect only classes that are defined in the same source file - and they
+ // are necessarily recompiled together with this class. So there is no point in initiating version
+ // compare for this class. However, the new class version should always tembe promoted into the store, since
+ // this class itself may depend on other changing classes.
+ pcde.checkResult = PCDEntry.CV_COMPATIBLE;
+ }
+
+ updatedAndCheckedClasses.add(className);
+ }
+ }
+
+ /** Find all dependent classes for deleted classes. */
+ private void checkDeletedClasses() {
+ for (String className : deletedClasses) {
+ PCDEntry pcde = pcd.get(className);
+
+ if (pcde == null) { // "Safety net" for the (hopefully unlikely) case. I observed it just once and couldn't identify the reason
+ Utils.printWarningMessage("Warning: internal information inconsistency when checking deleted classes");
+ Utils.printWarningMessage(Utils.REPORT_PROBLEM);
+ Utils.printWarningMessage("Class name: " + className);
+ continue;
+ }
+
+ ClassInfo oldCI = pcde.oldClassInfo;
+ if (!oldCI.isNonMemberNestedClass) { // See the comment above
+ Utils.printInfoMessage("Checking deleted class " + oldCI.name);
+ cv.checkDeletedClass(pcde);
+ String[] affectedClasses = cv.getAffectedClasses();
+ if (affectedClasses != null) {
+ for (int i = 0; i < affectedClasses.length; i++) {
+ PCDEntry affEntry = pcd.get(affectedClasses[i]);
+ if (deletedClasses.contains(affEntry.className)) {
+ continue;
+ }
+ updatedJavaFiles.add(affEntry.javaFileFullPath);
+ }
+ }
+ }
+ pcde.checkResult = PCDEntry.CV_DELETED;
+ updatedAndCheckedClasses.add(className);
+ }
+ deletedClasses.clear();
+ }
+
+ /**
+ * Determine what classes in the given .jar (which may be an existing updated one, or a new one) are new,
+ * updated, or moved, and treat them accordingly.
+ */
+ private void processAllClassesFromJarFile(String jarFileName) {
+ JarFile jarFile;
+ long jarFileLastMod = 0;
+ try {
+ File file = new File(jarFileName);
+ jarFileLastMod = file.lastModified();
+ jarFile = new JarFile(jarFileName);
+ } catch (IOException ex) {
+ throw new PrivateException(ex);
+ }
+
+ List<PCDEntry> newEntries = new ArrayList<PCDEntry>();
+ List<PCDEntry> updatedEntries = new ArrayList<PCDEntry>();
+ List<PCDEntry> movedEntries = new ArrayList<PCDEntry>();
+
+ for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
+ JarEntry jarEntry = entries.nextElement();
+ String fullClassName = jarEntry.getName();
+ if (!fullClassName.endsWith(".class")) {
+ continue;
+ }
+ fullClassName =
+ fullClassName.substring(0, fullClassName.length() - 6).intern();
+ byte classFileBytes[];
+ classFileBytes = Utils.readZipEntryIntoBuffer(jarFile, jarEntry);
+ long classFileFP = computeFP(classFileBytes);
+
+ PCDEntry pcde = pcd.get(fullClassName);
+ if (pcde != null) {
+ if (pcde.checked) {
+ throw new PrivateException(new PublicExceptions.DoubleEntryException(
+ "Two entries for class " + fullClassName + " detected: " + pcde.javaFileFullPath + " and " + jarFileName));
+ }
+ pcde.checked = true;
+ pcde.newClassFileLastModified = jarFileLastMod;
+ // If we are scanning an existing updated .jar file, and there is no change to the class itself,
+ // and it previously was located in the same .jar, do nothing.
+ if (pcde.oldClassFileFingerprint == classFileFP &&
+ pcde.javaFileFullPath.equals(jarFileName)) {
+ pcde.oldClassFileLastModified = jarFileLastMod; // So that next time jmake is inoked, checking
+ continue; // of this.jar is not triggered.
+ }
+ if (pcde.oldClassFileFingerprint != classFileFP) { // This class has been updated
+ updatedClasses.add(fullClassName);
+ allUpdatedClasses.add(fullClassName);
+ pcde.newClassFileLastModified = jarFileLastMod;
+ pcde.newClassFileFingerprint = classFileFP;
+ pcde.newClassInfo =
+ new ClassInfo(classFileBytes, ClassInfo.VER_NEW, this, fullClassName);
+ if (pcde.oldClassInfo.nestedClasses != null || pcde.newClassInfo.nestedClasses != null) {
+ updatedEntries.add(pcde);
+ }
+ } else {
+ pcde.oldClassFileLastModified = jarFileLastMod;
+ }
+ if (!pcde.javaFileFullPath.equals(jarFileName)) {
+ // Found an existing class in a different .jar file.
+ // May happen if the class file has been moved from one .jar to another (or into a .jar, losing its
+ // .java source). It's only at this point that we can actually see that it was really a move.
+ if (deletedClasses.contains(fullClassName)) {
+ deletedClasses.remove(fullClassName);
+ }
+ if (pcde.oldClassInfo.nestedClasses != null) {
+ movedEntries.add(pcde);
+ pcde.newClassInfo =
+ new ClassInfo(classFileBytes, ClassInfo.VER_NEW, this, fullClassName);
+ }
+ }
+ pcde.javaFileFullPath = jarFileName;
+ } else { // New class file
+ ClassInfo classInfo =
+ new ClassInfo(classFileBytes, ClassInfo.VER_NEW, this, fullClassName);
+ pcde = new PCDEntry(fullClassName,
+ jarFileName,
+ jarFileName, jarFileLastMod, classFileFP,
+ classInfo);
+ pcde.checkResult = PCDEntry.CV_NEW; // So that later it's promoted into oldClassInfo correctly
+ updatedAndCheckedClasses.add(fullClassName); // So that the above happens
+ pcd.put(fullClassName, pcde);
+ if (pcde.newClassInfo.nestedClasses != null) {
+ newEntries.add(pcde);
+ }
+ }
+ }
+
+ dealWithNestedClassesForUpdatedPCDEntries(updatedEntries, false);
+ dealWithNestedClassesForUpdatedPCDEntries(movedEntries, true);
+ for (int i = 0; i < newEntries.size(); i++) {
+ findAndUpdateAllNestedClassesForClass(newEntries.get(i), false);
+ }
+ }
+
+ /** Determine new, deleted and updated classes coming from updated .jar files. */
+ private void dealWithClassesInUpdatedJarFiles() {
+ if (updatedJarFiles.size() == 0) {
+ return;
+ }
+
+ for (String updatedJarFile : updatedJarFiles) {
+ processAllClassesFromJarFile(updatedJarFile);
+ }
+
+ // Now scan the PCD to check which classes that come from updated .jar files have not been marked as checked
+ for (PCDEntry pcde : entries()) {
+ if (updatedJarFiles.contains(pcde.javaFileFullPath)) {
+ if (!pcde.checked) {
+ deletedClasses.add(pcde.className);
+ }
+ }
+ }
+ }
+
+ /** Check if the destination directory exists, and get the canonical path for it. */
+ private void initializeDestDir(String inDestDir) {
+ if (!(inDestDir == null || inDestDir.equals(""))) {
+ File dir = Utils.checkOrCreateDirForName(inDestDir);
+ if (dir == null) {
+ throw new PrivateException(new IOException("specified directory " + inDestDir + " cannot be created."));
+ }
+ inDestDir = getCanonicalPath(dir);
+ if (!inDestDir.endsWith(File.separator)) {
+ inDestDir += File.separatorChar;
+ }
+ destDir = inDestDir;
+ destDirSpecified = true;
+ } else {
+ destDirSpecified = false;
+ }
+ }
+
+ /**
+ * For the given PCDEntry, set the entry.classFileFullPath according to the value of the .java file full
+ * path and the value of the "-d" option at this particular jmake invocation
+ */
+ private void initializeClassFileFullPath(PCDEntry entry) {
+ String classFileFullPath;
+ if (destDirSpecified) {
+ classFileFullPath = destDir + entry.className + ".class";
+ } else {
+ String javaFileDir = entry.javaFileFullPath;
+ int cutIndex = javaFileDir.lastIndexOf(File.separatorChar);
+ if (cutIndex != -1) {
+ javaFileDir = javaFileDir.substring(0, cutIndex + 1);
+ }
+ String classFileName = entry.className;
+ cutIndex = classFileName.lastIndexOf('/');
+ if (cutIndex != -1) {
+ classFileName = classFileName.substring(cutIndex + 1);
+ }
+ classFileFullPath = javaFileDir + classFileName + ".class";
+ }
+ if (backSlashFileSeparator) {
+ classFileFullPath =
+ classFileFullPath.replace('/', File.separatorChar);
+ }
+ entry.classFileFullPath = classFileFullPath;
+ }
+
+ private static String getCanonicalPath(File file) {
+ try {
+ return file.getCanonicalPath().intern();
+ } catch (IOException e) {
+ throw new PrivateException(e);
+ }
+ }
+
+ private long computeFP(File file) {
+ byte buf[] = Utils.readFileIntoBuffer(file);
+ return computeFP(buf);
+ }
+
+ private long computeFP(byte[] buf) {
+ checkSum.reset();
+ checkSum.update(buf);
+ return checkSum.getValue();
+ }
+
+ private PrivateException compilerInteractionException(String message, Exception origException, int errCode) {
+ return new PrivateException(new PublicExceptions.CompilerInteractionException(message, origException, errCode));
+ }
+
+ private PrivateException internalException(String message) {
+ return new PrivateException(new PublicExceptions.InternalException(message));
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698