1 /******************************************************************************* 2 * Copyright (c) 2005, 2018 BEA Systems, Inc. and others 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * mkaufman@bea.com - initial API and implementation 13 * wharley@bea.com - refactored, and reinstated reconcile-time type gen 14 *******************************************************************************/ 15 16 package org.eclipse.jdt.apt.core.internal.generatedfile; 17 18 import java.io.BufferedInputStream; 19 import java.io.ByteArrayInputStream; 20 import java.io.IOException; 21 import java.io.InputStream; 22 import java.util.ArrayList; 23 import java.util.Collection; 24 import java.util.HashMap; 25 import java.util.HashSet; 26 import java.util.Iterator; 27 import java.util.List; 28 import java.util.Map; 29 import java.util.Set; 30 import java.util.regex.Pattern; 31 32 import org.eclipse.core.resources.IContainer; 33 import org.eclipse.core.resources.IFile; 34 import org.eclipse.core.resources.IFolder; 35 import org.eclipse.core.resources.IMarker; 36 import org.eclipse.core.resources.IResource; 37 import org.eclipse.core.runtime.CoreException; 38 import org.eclipse.core.runtime.IPath; 39 import org.eclipse.core.runtime.IProgressMonitor; 40 import org.eclipse.jdt.apt.core.internal.AptPlugin; 41 import org.eclipse.jdt.apt.core.internal.AptProject; 42 import org.eclipse.jdt.apt.core.internal.Messages; 43 import org.eclipse.jdt.apt.core.internal.util.FileSystemUtil; 44 import org.eclipse.jdt.apt.core.internal.util.ManyToMany; 45 import org.eclipse.jdt.core.ElementChangedEvent; 46 import org.eclipse.jdt.core.ICompilationUnit; 47 import org.eclipse.jdt.core.IJavaModelStatusConstants; 48 import org.eclipse.jdt.core.IJavaProject; 49 import org.eclipse.jdt.core.IPackageFragment; 50 import org.eclipse.jdt.core.IPackageFragmentRoot; 51 import org.eclipse.jdt.core.JavaCore; 52 import org.eclipse.jdt.core.JavaModelException; 53 import org.eclipse.jdt.core.WorkingCopyOwner; 54 55 /** 56 * This class is used for managing generated files; in particular, keeping track of 57 * dependencies so that no-longer-generated files can be deleted, and managing the 58 * lifecycle of working copies in memory. 59 * <p> 60 * During build, a generated file may be a "type", in the sense of a generated Java source 61 * file, or it may be a generated class file or an arbitrary resource (such as an XML 62 * file). During reconcile, it is only possible to generate Java source files. Also, 63 * during reconcile, it is not possible to write to disk or delete files from disk; all 64 * operations take place in memory only, using "working copies" provided by the Java 65 * Model. 66 * 67 * <h2>DATA STRUCTURES</h2> 68 * <code>_buildDeps</code> is a many-to-many map that tracks which parent files 69 * are responsible for which generated files. Entries in this map are created when files 70 * are created during builds. This map is serialized so that dependencies can be reloaded 71 * when a project is opened without having to do a full build. 72 * <p> 73 * When types are generated during reconcile, they are not actually laid down on disk (ie 74 * we do not commit the working copy). However, the file handles are still used as keys 75 * into the various maps in this case. 76 * <p> 77 * <code>_reconcileDeps</code> is the reconcile-time analogue of 78 * <code>_buildDeps</code>. This map is not serialized. 79 * <p> 80 * Given a working copy, it is easy to determine the IFile that it models by calling 81 * <code>ICompilationUnit.getResource()</code>. To go the other way, we store maps 82 * of IFile to ICompilationUnit. Working copies that represent generated types are 83 * stored in <code>_reconcileGenTypes</code>; working copies that represent deleted types 84 * are stored in <code>_hiddenBuiltTypes</code>. 85 * <p> 86 * List invariants: for the many-to-many maps, every forward entry must correspond to a 87 * reverse entry; this is managed (and verified) by the ManyToMany map code. Also, every 88 * entry in the <code>_reconcileGenTypes</code> list must correspond to an entry in the 89 * <code>_reconcileDeps</code> map. There can be no overlap between these 90 * entries and the <code>_hiddenBuiltTypes</code> map. Whenever a working copy is placed 91 * into this overall collection, it must have <code>becomeWorkingCopy()</code> called on 92 * it; whenever it is removed, it must have <code>discardWorkingCopy()</code> called on 93 * it. 94 * 95 * <h2>SYNCHRONIZATION NOTES</h2> 96 * Synchronization around the GeneratedFileManager's maps uses the GeneratedFileMap 97 * instance's monitor. When acquiring this monitor, DO NOT PERFORM ANY OPERATIONS THAT 98 * TAKE ANY OTHER LOCKS (e.g., java model operations, or file system operations like 99 * creating or deleting a file or folder). If you do this, then the code is subject to 100 * deadlock situations. For example, a resource-changed listener may take a resource lock 101 * and then call into the GeneratedFileManager for clean-up, where your code could reverse 102 * the order in which the locks are taken. This is bad, so be careful. 103 * 104 * <h2>RECONCILE vs. BUILD</h2> 105 * Reconciles are based on in-memory type information, i.e., working copies. Builds are 106 * based on files on disk. At any given moment, a build thread and any number of reconcile 107 * threads may be executing. All share the same GeneratedFileManager object, but each 108 * thread will have a separate BuildEnvironment. Multiple files are built in a loop, with 109 * files generated on one round being compiled (and possibly generating new files) on the 110 * next; only one file at a time is reconciled, but when a file is generated during 111 * reconcile it will invoke a recursive call to reconcile, with a unique 112 * ReconcileBuildEnvironment. 113 * <p> 114 * What is the relationship between reconcile-time dependency information and build-time 115 * dependency information? In general, there is one set of dependency maps for build time 116 * information and a separate set for reconcile time information (with the latter being 117 * shared by all reconcile threads). Reconciles do not write to build-time information, 118 * nor do they write to the disk. Builds, however, may have to interact with 119 * reconcile-time info. The tricky bit is that a change to a file "B.java" in the 120 * foreground editor window might affect the way that background file "A.java" generates 121 * "AGen.java". That is, editing B.java is in effect making A.java dirty; but the Eclipse 122 * build system has no way of knowing that, so A will not be reconciled. 123 * <p> 124 * The nearest Eclipse analogy is to refactoring, where a refactor operation in the 125 * foreground editor can modify background files; Eclipse solves this problem by requiring 126 * that all files be saved before and after a refactoring operation, but that solution is 127 * not practical for the simple case of making edits to a file that might happen to be an 128 * annotation processing dependency. The JSR269 API works around this problem by letting 129 * processors state these out-of-band dependencies explicitly, but com.sun.mirror.apt has 130 * no such mechanism. 131 * <p> 132 * The approach taken here is that when a build is performed, we discard the working 133 * copies of any files that are open in editors but that are not dirty (meaning the file 134 * on disk is the same as the version in the editor). This still means that if file A is 135 * dirty, AGen will not be updated even when B is edited; thus, making a breaking change 136 * to A and then making a change to B that is supposed to fix the break will not work. 137 */ 138 public class GeneratedFileManager 139 { 140 141 /** 142 * Access to the package fragment root for generated types. 143 * Encapsulated into this class so that synchronization can be guaranteed. 144 */ 145 private class GeneratedPackageFragmentRoot { 146 147 // The name and root are returned as a single object to ensure synchronization. 148 final class NameAndRoot { 149 final String name; 150 final IPackageFragmentRoot root; NameAndRoot(String name, IPackageFragmentRoot root)151 NameAndRoot(String name, IPackageFragmentRoot root) { 152 this.name = name; 153 this.root = root; 154 } 155 } 156 157 private IPackageFragmentRoot _root = null; 158 159 private String _folderName = null; 160 161 /** 162 * Get the package fragment root and the name of the folder 163 * it corresponds to. If the folder is not on the classpath, 164 * the root will be null. 165 */ get()166 public synchronized NameAndRoot get() { 167 return new NameAndRoot(_folderName, _root); 168 } 169 170 /** 171 * Force the package fragment root and folder name to be recalculated. 172 * Check whether the new folder is actually on the classpath; if not, 173 * set root to be null. 174 */ set()175 public synchronized void set() { 176 IFolder genFolder = _gsfm.getFolder(); 177 _root = null; 178 if (_jProject.isOnClasspath(genFolder)) { 179 _root = _jProject.getPackageFragmentRoot(genFolder); 180 } 181 _folderName = genFolder.getProjectRelativePath().toString(); 182 } 183 } 184 185 /** 186 * If true, when buffer contents are updated during a reconcile, reconcile() will 187 * be called on the new contents. This is not necessary to update the open editor, 188 * but if the generated file is itself a parent file, it will cause recursive 189 * type generation. 190 */ 191 private static final boolean RECURSIVE_RECONCILE = true; 192 193 /** 194 * Disable type generation during reconcile. In the past, reconcile-time type 195 * generation caused deadlocks; see (BEA internal) Radar bug #238684. As of 196 * Eclipse 3.3 this should work. 197 */ 198 private static final boolean GENERATE_TYPE_DURING_RECONCILE = true; 199 200 /** 201 * If true, the integrity of internal data structures will be verified after various 202 * operations are performed. 203 */ 204 private static final boolean ENABLE_INTEGRITY_CHECKS = true; 205 206 /** 207 * A singleton instance of CompilationUnitHelper, which encapsulates operations on working copies. 208 */ 209 private static final CompilationUnitHelper _CUHELPER = new CompilationUnitHelper(); 210 211 /** 212 * The regex delimiter used to parse package names. 213 */ 214 private static final Pattern _PACKAGE_DELIMITER = Pattern.compile("\\."); //$NON-NLS-1$ 215 216 static { 217 // register element-changed listener to clean up working copies 218 int mask = ElementChangedEvent.POST_CHANGE; JavaCore.addElementChangedListener(new WorkingCopyCleanupListener(), mask)219 JavaCore.addElementChangedListener(new WorkingCopyCleanupListener(), mask); 220 } 221 222 /** 223 * Many-to-many map from parent files to files generated during build. These files all 224 * exist on disk. This map is used to keep track of dependencies during build, and is 225 * read-only during reconcile. This map is serialized. 226 */ 227 private final GeneratedFileMap _buildDeps; 228 229 /** 230 * Set of files that have been generated during build by processors that 231 * support reconcile-time type generation. Files in this set are expected to 232 * be generated during reconcile, and therefore will be deleted after a reconcile 233 * if they're not generated. This is different from the value set of 234 * _reconcileDeps in that the contents of this set are known to have been 235 * generated during a build. 236 */ 237 private final Set<IFile> _clearDuringReconcile; 238 239 /** 240 * Many-to-many map from parent files to files generated during reconcile. 241 * Both the keys and the values may correspond to files that exist on disk or only in 242 * memory. This map is used to keep track of dependencies created during reconcile, 243 * and is not accessed during build. This map is not serialized. 244 */ 245 private final ManyToMany<IFile, IFile> _reconcileDeps; 246 247 /** 248 * Many-to-many map from parent files to files that are generated in build but not 249 * during reconcile. We need this so we can tell parents that were never reconciled 250 * (meaning their generated children on disk are valid) from parents that have been 251 * edited so that they no longer generate their children (meaning the generated 252 * children may need to be removed from the typesystem). This map is not serialized. 253 */ 254 private final ManyToMany<IFile, IFile> _reconcileNonDeps; 255 256 /** 257 * Map of types that were generated during build but are being hidden (removed from 258 * the reconcile-time typesystem) by blank WorkingCopies. These are tracked separately 259 * from regular working copies for the sake of clarity. The keys all correspond to 260 * files that exist on disk; if they didn't, there would be no reason for an entry. 261 * <p> 262 * This is a map of file to working copy of that file, <strong>NOT</strong> a map of 263 * parents to generated children. The keys in this map are a subset of the values in 264 * {@link #_reconcileNonDeps}. This map exists so that given a file, we can find the 265 * working copy that represents it. 266 * <p> 267 * Every working copy exists either in this map or in {@link #_hiddenBuiltTypes}, but 268 * not in both. These maps exist to track the lifecycle of a working copy. When a new 269 * working copy is created, {@link ICompilationUnit#becomeWorkingCopy()} is called. If 270 * an entry is removed from this map without being added to the other, 271 * {@link ICompilationUnit#discardWorkingCopy()} must be called. 272 * 273 * @see #_reconcileGenTypes 274 */ 275 private final Map<IFile, ICompilationUnit> _hiddenBuiltTypes; 276 277 /** 278 * Cache of working copies (in-memory types created or modified during reconcile). 279 * Includes working copies that represent changes to types that were generated during 280 * a build and thus exist on disk, as well as working copies for types newly generated 281 * during reconcile that thus do not exist on disk. 282 * <p> 283 * This is a map of file to working copy of that file, <strong>NOT</strong> a map of 284 * parents to generated children. There is a 1:1 correspondence between keys in this 285 * map and values in {@link #_reconcileDeps}. This map exists so that given a file, 286 * we can find the working copy that represents it. 287 * <p> 288 * Every working copy exists either in this map or in {@link #_hiddenBuiltTypes}, but 289 * not in both. These maps exist to track the lifecycle of a working copy. When a new 290 * working copy is created, {@link ICompilationUnit#becomeWorkingCopy()} is called. If 291 * an entry is removed from this map without being added to the other, 292 * {@link ICompilationUnit#discardWorkingCopy()} must be called. 293 * 294 * @see #_hiddenBuiltTypes 295 */ 296 private final Map<IFile, ICompilationUnit> _reconcileGenTypes; 297 298 /** 299 * Access to the package fragment root for generated types. Encapsulated into a 300 * helper class in order to ensure synchronization. 301 */ 302 private final GeneratedPackageFragmentRoot _generatedPackageFragmentRoot; 303 304 private final IJavaProject _jProject; 305 306 private final GeneratedSourceFolderManager _gsfm; 307 308 /** 309 * Initialized when the build starts, and accessed during type generation. 310 * This has the same lifecycle as _generatedPackageFragmentRoot. 311 * If there is a configuration problem, this may be set to <code>true</code> 312 * during generation of the first type to prevent any other types from 313 * being generated. 314 */ 315 private boolean _skipTypeGeneration = false; 316 317 /** 318 * Clients should not instantiate this class; it is created only by {@link AptProject}. 319 */ GeneratedFileManager(final AptProject aptProject, final GeneratedSourceFolderManager gsfm)320 public GeneratedFileManager(final AptProject aptProject, final GeneratedSourceFolderManager gsfm) { 321 _jProject = aptProject.getJavaProject(); 322 _gsfm = gsfm; 323 _buildDeps = new GeneratedFileMap(_jProject.getProject(), gsfm.isTestCode()); 324 _clearDuringReconcile = new HashSet<>(); 325 _reconcileDeps = new ManyToMany<>(); 326 _reconcileNonDeps = new ManyToMany<>(); 327 _hiddenBuiltTypes = new HashMap<>(); 328 _reconcileGenTypes = new HashMap<>(); 329 _generatedPackageFragmentRoot = new GeneratedPackageFragmentRoot(); 330 } 331 332 /** 333 * Add a non-Java-source entry to the build-time dependency maps. Java source files are added to 334 * the maps when they are generated, as by {@link #generateFileDuringBuild}, but files of other 335 * types must be added explicitly by the code that creates the file. 336 * <p> 337 * This method must only be called during build, not reconcile. It is not possible to add 338 * non-Java-source files during reconcile. 339 */ addGeneratedFileDependency(Collection<IFile> parentFiles, IFile generatedFile)340 public void addGeneratedFileDependency(Collection<IFile> parentFiles, IFile generatedFile) 341 { 342 addBuiltFileToMaps(parentFiles, generatedFile, false); 343 } 344 345 /** 346 * Called at the start of build in order to cache our package fragment root 347 */ compilationStarted()348 public void compilationStarted() 349 { 350 try { 351 // clear out any generated source folder config markers 352 if(!_gsfm.isTestCode()) { 353 IMarker[] markers = _jProject.getProject().findMarkers(AptPlugin.APT_CONFIG_PROBLEM_MARKER, true, 354 IResource.DEPTH_INFINITE); 355 if (markers != null) { 356 for (IMarker marker : markers) 357 marker.delete(); 358 } 359 } 360 } catch (CoreException e) { 361 AptPlugin.log(e, "Unable to delete configuration marker."); //$NON-NLS-1$ 362 } 363 _skipTypeGeneration = false; 364 _gsfm.ensureFolderExists(); 365 _generatedPackageFragmentRoot.set(); 366 367 } 368 369 /** 370 * This method should only be used for testing purposes to ensure that maps contain 371 * entries when we expect them to. 372 */ containsWorkingCopyMapEntriesForParent(IFile f)373 public synchronized boolean containsWorkingCopyMapEntriesForParent(IFile f) 374 { 375 return _reconcileDeps.containsKey(f); 376 } 377 378 /** 379 * Invoked at the end of a build to delete files that are no longer parented by 380 * <code>parentFile</code>. Files that are multiply parented will not actually be 381 * deleted, but the association from this parent to the generated file will be 382 * removed, so that when the last parent ceases to generate a given file it will be 383 * deleted at that time. 384 * 385 * @param newlyGeneratedFiles 386 * the set of files generated by <code>parentFile</code> on the most 387 * recent compilation; these files will be spared deletion. 388 * @return the set of source files that were actually deleted, or an empty set. 389 * The returned set does not include non-source (e.g. text or xml) files. 390 */ deleteObsoleteFilesAfterBuild(IFile parentFile, Set<IFile> newlyGeneratedFiles)391 public Set<IFile> deleteObsoleteFilesAfterBuild(IFile parentFile, Set<IFile> newlyGeneratedFiles) 392 { 393 Set<IFile> deleted; 394 List<ICompilationUnit> toDiscard = new ArrayList<>(); 395 Set<IFile> toReport = new HashSet<>(); 396 deleted = computeObsoleteFiles(parentFile, newlyGeneratedFiles, toDiscard, toReport); 397 398 for (IFile toDelete : deleted) { 399 if (AptPlugin.DEBUG_GFM) AptPlugin.trace( 400 "deleted obsolete file during build: " + toDelete); //$NON-NLS-1$ 401 deletePhysicalFile(toDelete); 402 } 403 404 // Discard blank WCs *after* we delete the corresponding files: 405 // we don't want the type to become briefly visible to a reconcile thread. 406 for (ICompilationUnit wcToDiscard : toDiscard) { 407 _CUHELPER.discardWorkingCopy(wcToDiscard); 408 } 409 410 return toReport; 411 } 412 413 /** 414 * Invoked at the end of a reconcile to get rid of any files that are no longer being 415 * generated. If the file existed on disk, we can't actually delete it, we can only 416 * create a blank WorkingCopy to hide it. Therefore, we can only remove Java source 417 * files, not arbitrary files. If the file was generated during reconcile and exists 418 * only in memory, we can actually remove it altogether. 419 * <p> 420 * Only some processors specify (via {@link org.eclipse.jdt.apt.core.util.AptPreferenceConstants#RTTG_ENABLED_OPTION}) 421 * that they support type generation during reconcile. We need to remove obsolete 422 * files generated by those processors, but preserve files generated by 423 * other processors. 424 * 425 * @param parentWC 426 * the WorkingCopy being reconciled 427 * @param newlyGeneratedFiles 428 * the complete list of files generated during the reconcile (including 429 * files that exist on disk as well as files that only exist in memory) 430 */ deleteObsoleteTypesAfterReconcile(ICompilationUnit parentWC, Set<IFile> newlyGeneratedFiles)431 public void deleteObsoleteTypesAfterReconcile(ICompilationUnit parentWC, Set<IFile> newlyGeneratedFiles) 432 { 433 IFile parentFile = (IFile) parentWC.getResource(); 434 435 List<ICompilationUnit> toSetBlank = new ArrayList<>(); 436 List<ICompilationUnit> toDiscard = new ArrayList<>(); 437 computeObsoleteReconcileTypes(parentFile, newlyGeneratedFiles, _CUHELPER, toSetBlank, toDiscard); 438 439 for (ICompilationUnit wcToDiscard : toDiscard) { 440 if (AptPlugin.DEBUG_GFM) AptPlugin.trace( 441 "discarded obsolete working copy during reconcile: " + wcToDiscard.getElementName()); //$NON-NLS-1$ 442 _CUHELPER.discardWorkingCopy(wcToDiscard); 443 } 444 445 WorkingCopyOwner workingCopyOwner = parentWC.getOwner(); 446 for (ICompilationUnit wcToSetBlank : toSetBlank) { 447 if (AptPlugin.DEBUG_GFM) AptPlugin.trace( 448 "hiding file with blank working copy during reconcile: " + wcToSetBlank.getElementName()); //$NON-NLS-1$ 449 _CUHELPER.updateWorkingCopyContents("", wcToSetBlank, workingCopyOwner, RECURSIVE_RECONCILE); //$NON-NLS-1$ 450 } 451 452 assert checkIntegrity(); 453 } 454 455 /** 456 * Called by the resource change listener when a file is deleted (eg by the user). 457 * Removes any files parented by this file, and removes the file from dependency maps 458 * if it is generated. This does not remove working copies parented by the file; that 459 * will happen when the working copy corresponding to the parent file is discarded. 460 * 461 * @param f 462 */ fileDeleted(IFile f)463 public void fileDeleted(IFile f) 464 { 465 List<IFile> toDelete = removeFileFromBuildMaps(f); 466 467 for (IFile fileToDelete : toDelete) { 468 deletePhysicalFile(fileToDelete); 469 } 470 471 } 472 473 /** 474 * Invoked when a file is generated during a build. The generated file and 475 * intermediate directories will be created if they don't exist. This method takes 476 * file-system locks, and assumes that the calling method has at some point acquired a 477 * workspace-level resource lock. 478 * 479 * @param parentFiles 480 * the parent or parents of the type being generated. May be empty, and/or 481 * may contain null entries, but must not itself be null. 482 * @param typeName 483 * the dot-separated java type name of the type being generated 484 * @param contents 485 * the java code contents of the new type . 486 * @param clearDuringReconcile 487 * if true, this file should be removed after any reconcile in which it was not 488 * regenerated. This typically is used when the file is being generated by a 489 * processor that supports {@linkplain org.eclipse.jdt.apt.core.util.AptPreferenceConstants#RTTG_ENABLED_OPTION 490 * reconcile-time type generation}. 491 * @param progressMonitor 492 * a progress monitor. This may be null. 493 * @return - the newly created IFile along with whether it was modified 494 * @throws CoreException 495 */ generateFileDuringBuild(Collection<IFile> parentFiles, String typeName, String contents, boolean clearDuringReconcile, IProgressMonitor progressMonitor)496 public FileGenerationResult generateFileDuringBuild(Collection<IFile> parentFiles, String typeName, String contents, 497 boolean clearDuringReconcile, IProgressMonitor progressMonitor) throws CoreException 498 { 499 if (_skipTypeGeneration) 500 return null; 501 502 GeneratedPackageFragmentRoot.NameAndRoot gpfr = _generatedPackageFragmentRoot.get(); 503 IPackageFragmentRoot root = gpfr.root; 504 if (root == null) { 505 // If the generated package fragment root wasn't set, then our classpath 506 // is incorrect. Add a marker and return. We do this here, rather than in 507 // the set() method, because if they're not going to generate any types 508 // then it doesn't matter that the classpath is wrong. 509 String message = Messages.bind(Messages.GeneratedFileManager_missing_classpath_entry, 510 new String[] { gpfr.name }); 511 IMarker marker = _jProject.getProject().createMarker(AptPlugin.APT_CONFIG_PROBLEM_MARKER); 512 marker.setAttributes(new String[] { IMarker.MESSAGE, IMarker.SEVERITY, IMarker.SOURCE_ID }, 513 new Object[] { message, IMarker.SEVERITY_ERROR, AptPlugin.APT_MARKER_SOURCE_ID }); 514 // disable any future type generation 515 _skipTypeGeneration = true; 516 return null; 517 } 518 519 // Do the new contents differ from what is already on disk? 520 // We need to know so we can tell the caller whether this is a modification. 521 IFile file = getIFileForTypeName(typeName); 522 boolean contentsDiffer = compareFileContents(contents, file); 523 524 try { 525 if (contentsDiffer) { 526 final String[] names = parseTypeName(typeName); 527 final String pkgName = names[0]; 528 final String cuName = names[1]; 529 530 // Get a list of the folders that will have to be created for this package to exist 531 IFolder genSrcFolder = (IFolder) root.getResource(); 532 final Set<IFolder> newFolders = computeNewPackageFolders(pkgName, genSrcFolder); 533 534 // Create the package fragment in the Java Model. This creates all needed parent folders. 535 IPackageFragment pkgFrag = _CUHELPER.createPackageFragment(pkgName, root, progressMonitor); 536 537 // Mark all newly created folders (but not pre-existing ones) as derived. 538 for (IContainer folder : newFolders) { 539 try { 540 folder.setDerived(true, progressMonitor); 541 } catch (CoreException e) { 542 AptPlugin.logWarning(e, "Unable to mark generated type folder as derived: " + folder.getName()); //$NON-NLS-1$ 543 break; 544 } 545 } 546 547 saveCompilationUnit(pkgFrag, cuName, contents, progressMonitor); 548 } 549 550 // during a batch build, parentFile will be null. 551 // Only keep track of ownership in iterative builds 552 addBuiltFileToMaps(parentFiles, file, true); 553 if (clearDuringReconcile) { 554 _clearDuringReconcile.add(file); 555 } 556 557 // Mark the file as derived. Note that certain user actions may have 558 // deleted this file before we get here, so if the file doesn't 559 // exist, marking it derived throws a ResourceException. 560 if (file.exists()) { 561 file.setDerived(true, progressMonitor); 562 } 563 // We used to also make the file read-only. This is a bad idea, 564 // as refactorings then fail in the future, which is worse 565 // than allowing a user to modify a generated file. 566 567 assert checkIntegrity(); 568 569 return new FileGenerationResult(file, contentsDiffer); 570 } catch (CoreException e) { 571 AptPlugin.log(e, "Unable to generate type " + typeName); //$NON-NLS-1$ 572 return null; 573 } 574 } 575 576 /** 577 * This function generates a type "in-memory" by creating or updating a working copy 578 * with the specified contents. The generated-source folder must be configured 579 * correctly for this to work. This method takes no locks, so it is safe to call when 580 * holding fine-grained resource locks (e.g., during some reconcile paths). Since this 581 * only works on an in-memory working copy of the type, the IFile for the generated 582 * type might not exist on disk. Likewise, the corresponding package directories of 583 * type-name might not exist on disk. 584 * 585 * TODO: figure out how to create a working copy with a client-specified character set 586 * 587 * @param parentCompilationUnit the parent compilation unit. 588 * @param typeName the dot-separated java type name for the new type 589 * @param contents the contents of the new type 590 * @return The FileGenerationResult. This will return null if the generated source 591 * folder is not configured, or if there is some other error during type 592 * generation. 593 * 594 */ generateFileDuringReconcile(ICompilationUnit parentCompilationUnit, String typeName, String contents)595 public FileGenerationResult generateFileDuringReconcile(ICompilationUnit parentCompilationUnit, String typeName, 596 String contents) throws CoreException 597 { 598 if (!GENERATE_TYPE_DURING_RECONCILE) 599 return null; 600 601 IFile parentFile = (IFile) parentCompilationUnit.getResource(); 602 603 ICompilationUnit workingCopy = getWorkingCopyForReconcile(parentFile, typeName, _CUHELPER); 604 605 // Update its contents and recursively reconcile 606 boolean modified = _CUHELPER.updateWorkingCopyContents( 607 contents, workingCopy, parentCompilationUnit.getOwner(), RECURSIVE_RECONCILE); 608 if (AptPlugin.DEBUG_GFM) { 609 if (modified) 610 AptPlugin.trace("working copy modified during reconcile: " + typeName); //$NON-NLS-1$ 611 else 612 AptPlugin.trace("working copy unmodified during reconcile: " + typeName); //$NON-NLS-1$ 613 } 614 615 IFile generatedFile = (IFile) workingCopy.getResource(); 616 return new FileGenerationResult(generatedFile, modified); 617 } 618 619 /** 620 * @param parent - 621 * the parent file that you want to get generated files for 622 * @return Set of IFile instances that are the files known to be generated by this 623 * parent, or an empty collection if there are none. 624 * 625 * @see #isParentFile(IFile) 626 * @see #isGeneratedFile(IFile) 627 */ getGeneratedFilesForParent(IFile parent)628 public synchronized Set<IFile> getGeneratedFilesForParent(IFile parent) 629 { 630 return _buildDeps.getValues(parent); 631 } 632 633 /** 634 * returns true if the specified file is a generated file (i.e., it has one or more 635 * parent files) 636 * 637 * @param f 638 * the file in question 639 * @return true 640 */ isGeneratedFile(IFile f)641 public synchronized boolean isGeneratedFile(IFile f) 642 { 643 return _buildDeps.containsValue(f); 644 } 645 646 647 648 /** 649 * returns true if the specified file is a parent file (i.e., it has one or more 650 * generated files) 651 * 652 * @param f - 653 * the file in question 654 * @return true if the file is a parent, false otherwise 655 * 656 * @see #getGeneratedFilesForParent(IFile) 657 * @see #isGeneratedFile(IFile) 658 */ isParentFile(IFile f)659 public synchronized boolean isParentFile(IFile f) 660 { 661 return _buildDeps.containsKey(f); 662 } 663 664 /** 665 * Perform the actions necessary to respond to a clean. 666 */ projectCleaned()667 public void projectCleaned() { 668 Iterable<ICompilationUnit> toDiscard = computeClean(); 669 for (ICompilationUnit wc : toDiscard) { 670 _CUHELPER.discardWorkingCopy(wc); 671 } 672 if (AptPlugin.DEBUG_GFM_MAPS) AptPlugin.trace( 673 "cleared build file dependencies"); //$NON-NLS-1$ 674 } 675 676 /** 677 * Perform the actions necessary to respond to a project being closed. 678 * Throw out the reconcile-time information and working copies; throw 679 * out the build-time dependency information but leave its serialized 680 * version on disk in case the project is re-opened. 681 */ projectClosed()682 public void projectClosed() 683 { 684 if (AptPlugin.DEBUG_GFM) AptPlugin.trace("discarding working copy state"); //$NON-NLS-1$ 685 List<ICompilationUnit> toDiscard; 686 toDiscard = computeProjectClosed(false); 687 for (ICompilationUnit wc : toDiscard) { 688 _CUHELPER.discardWorkingCopy(wc); 689 } 690 } 691 692 /** 693 * Perform the actions necessary to respond to a project being deleted. 694 * Throw out everything related to the project, including its serialized 695 * build dependencies. 696 */ projectDeleted()697 public void projectDeleted() 698 { 699 if (AptPlugin.DEBUG_GFM) AptPlugin.trace("discarding all state"); //$NON-NLS-1$ 700 List<ICompilationUnit> toDiscard; 701 toDiscard = computeProjectClosed(true); 702 for (ICompilationUnit wc : toDiscard) { 703 _CUHELPER.discardWorkingCopy(wc); 704 } 705 } 706 707 /** 708 * Called at the start of reconcile in order to cache our package fragment root 709 */ reconcileStarted()710 public void reconcileStarted() 711 { 712 _generatedPackageFragmentRoot.set(); 713 } 714 715 /** 716 * Invoked when a working copy is released, ie, an editor is closed. This 717 * includes IDE shutdown. 718 * 719 * @param wc 720 * must not be null, but does not have to be a parent. 721 * @throws CoreException 722 */ workingCopyDiscarded(ICompilationUnit wc)723 public void workingCopyDiscarded(ICompilationUnit wc) throws CoreException 724 { 725 List<ICompilationUnit> toDiscard = removeFileFromReconcileMaps((IFile)(wc.getResource())); 726 if (AptPlugin.DEBUG_GFM) AptPlugin.trace( 727 "Working copy discarded: " + wc.getElementName() + //$NON-NLS-1$ 728 " removing " + toDiscard.size() + " children"); //$NON-NLS-1$//$NON-NLS-2$ 729 for (ICompilationUnit obsoleteWC : toDiscard) { 730 _CUHELPER.discardWorkingCopy(obsoleteWC); 731 } 732 } 733 734 /** 735 * Serialize the generated file dependency data for builds, so that when a workspace 736 * is reopened, incremental builds will work correctly. 737 */ writeState()738 public void writeState() 739 { 740 _buildDeps.writeState(); 741 } 742 743 /** 744 * Add a file dependency at build time. This updates the build dependency map but does 745 * not affect the reconcile-time dependencies. 746 * <p> 747 * This method only affects maps; it does not touch disk or modify working copies. 748 * 749 * @param isSource true for source files that will be compiled; false for non-source, e.g., text or xml. 750 */ addBuiltFileToMaps(Collection<IFile> parentFiles, IFile generatedFile, boolean isSource)751 private synchronized void addBuiltFileToMaps(Collection<IFile> parentFiles, IFile generatedFile, boolean isSource) 752 { 753 // during a batch build, parentFile will be null. 754 // Only keep track of ownership in iterative builds 755 for (IFile parentFile : parentFiles) { 756 if (parentFile != null) { 757 boolean added = _buildDeps.put(parentFile, generatedFile, isSource); 758 if (AptPlugin.DEBUG_GFM_MAPS) { 759 if (added) 760 AptPlugin.trace("build file dependency added: " + parentFile + " -> " + generatedFile); //$NON-NLS-1$//$NON-NLS-2$ 761 else 762 AptPlugin.trace("build file dependency already exists: " + parentFile + " -> " + generatedFile); //$NON-NLS-1$//$NON-NLS-2$ 763 } 764 } 765 } 766 } 767 768 /** 769 * Check integrity of data structures. 770 * @return true always, so that it can be called within an assert to turn it off at runtime 771 */ checkIntegrity()772 private synchronized boolean checkIntegrity() throws IllegalStateException 773 { 774 if (!ENABLE_INTEGRITY_CHECKS || !AptPlugin.DEBUG_GFM_MAPS) { 775 return true; 776 } 777 778 // There is a 1:1 correspondence between values in _reconcileDeps and 779 // keys in _reconcileGenTypes. 780 Set<IFile> depChildren = _reconcileDeps.getValueSet(); // copy - safe to modify 781 Set<IFile> genTypes = _reconcileGenTypes.keySet(); // not a copy! 782 List<IFile> extraFiles = new ArrayList<>(); 783 for (IFile f : genTypes) { 784 if (!depChildren.remove(f)) { 785 extraFiles.add(f); 786 } 787 } 788 if (!extraFiles.isEmpty()) { 789 logExtraFiles("File(s) in reconcile-generated list but not in reconcile dependency map: ", //$NON-NLS-1$ 790 extraFiles); 791 } 792 if (!depChildren.isEmpty()) { 793 logExtraFiles("File(s) in reconcile dependency map but not in reconcile-generated list: ", //$NON-NLS-1$ 794 depChildren); 795 } 796 797 // Every file in _clearDuringReconcile must be a value in _buildDeps. 798 List<IFile> extraClearDuringReconcileFiles = new ArrayList<>(); 799 for (IFile clearDuringReconcile : _clearDuringReconcile) { 800 if (!_buildDeps.containsValue(clearDuringReconcile)) { 801 extraClearDuringReconcileFiles.add(clearDuringReconcile); 802 } 803 } 804 if (!extraClearDuringReconcileFiles.isEmpty()) { 805 logExtraFiles("File(s) in list to clear during reconcile but not in build dependency map: ", //$NON-NLS-1$ 806 extraClearDuringReconcileFiles); 807 } 808 809 // Every key in _hiddenBuiltTypes must be a value in _reconcileNonDeps. 810 List<IFile> extraHiddenTypes = new ArrayList<>(); 811 for (IFile hidden : _hiddenBuiltTypes.keySet()) { 812 if (!_reconcileNonDeps.containsValue(hidden)) { 813 extraHiddenTypes.add(hidden); 814 } 815 } 816 if (!extraHiddenTypes.isEmpty()) { 817 logExtraFiles("File(s) in hidden types list but not in reconcile-obsoleted list: ", //$NON-NLS-1$ 818 extraHiddenTypes); 819 } 820 821 // There can be no parent/child pairs that exist in both _reconcileDeps 822 // and _reconcileNonDeps. 823 Map<IFile, IFile> reconcileOverlaps = new HashMap<>(); 824 for (IFile parent : _reconcileNonDeps.getKeySet()) { 825 for (IFile child : _reconcileNonDeps.getValues(parent)) { 826 if (_reconcileDeps.containsKeyValuePair(parent, child)) { 827 reconcileOverlaps.put(parent, child); 828 } 829 } 830 } 831 if (!reconcileOverlaps.isEmpty()) { 832 logExtraFilePairs("Entries exist in both reconcile map and reconcile-obsoleted maps: ", //$NON-NLS-1$ 833 reconcileOverlaps); 834 } 835 836 // Every parent/child pair in _reconcileNonDeps must have a matching 837 // parent/child pair in _buildDeps. 838 Map<IFile, IFile> extraNonDeps = new HashMap<>(); 839 for (IFile parent : _reconcileNonDeps.getKeySet()) { 840 for (IFile child : _reconcileNonDeps.getValues(parent)) { 841 if (!_buildDeps.containsKeyValuePair(parent, child)) { 842 extraNonDeps.put(parent, child); 843 } 844 } 845 } 846 if (!extraNonDeps.isEmpty()) { 847 logExtraFilePairs("Entries exist in reconcile-obsoleted map but not in build map: ", //$NON-NLS-1$ 848 extraNonDeps); 849 } 850 851 // Values in _hiddenBuiltTypes must not be null 852 List<IFile> nullHiddenTypes = new ArrayList<>(); 853 for (Map.Entry<IFile, ICompilationUnit> entry : _hiddenBuiltTypes.entrySet()) { 854 if (entry.getValue() == null) { 855 nullHiddenTypes.add(entry.getKey()); 856 } 857 } 858 if (!nullHiddenTypes.isEmpty()) { 859 logExtraFiles("Null entries in hidden type list: ", nullHiddenTypes); //$NON-NLS-1$ 860 } 861 862 // Values in _reconcileGenTypes must not be null 863 List<IFile> nullReconcileTypes = new ArrayList<>(); 864 for (Map.Entry<IFile, ICompilationUnit> entry : _reconcileGenTypes.entrySet()) { 865 if (entry.getValue() == null) { 866 nullReconcileTypes.add(entry.getKey()); 867 } 868 } 869 if (!nullReconcileTypes.isEmpty()) { 870 logExtraFiles("Null entries in reconcile type list: ", nullReconcileTypes); //$NON-NLS-1$ 871 } 872 873 return true; 874 } 875 876 /** 877 * Clear the working copy maps, that is, the reconcile-time dependency information. 878 * Returns a list of working copies that are no longer referenced and should be 879 * discarded. Typically called when a project is being closed or deleted. 880 * <p> 881 * It's not obvious we actually need this. As long as the IDE discards the parent 882 * working copies before the whole GeneratedFileManager is discarded, there'll be 883 * nothing left to clear by the time we get here. This is a "just in case." 884 * <p> 885 * This method affects maps only; it does not touch disk nor create, modify, nor 886 * discard any working copies. This method is atomic with respect to data structure 887 * integrity. 888 * 889 * @param deleteState 890 * true if this should delete the serialized build dependencies. 891 * 892 * @return a list of working copies which must be discarded by the caller 893 */ computeProjectClosed(boolean deleteState)894 private synchronized List<ICompilationUnit> computeProjectClosed(boolean deleteState) 895 { 896 int size = _hiddenBuiltTypes.size() + _reconcileGenTypes.size(); 897 List<ICompilationUnit> toDiscard = new ArrayList<>(size); 898 toDiscard.addAll(_hiddenBuiltTypes.values()); 899 toDiscard.addAll(_reconcileGenTypes.values()); 900 _reconcileGenTypes.clear(); 901 _hiddenBuiltTypes.clear(); 902 _reconcileDeps.clear(); 903 _reconcileNonDeps.clear(); 904 905 if (deleteState) { 906 _buildDeps.clearState(); 907 } 908 else { 909 _buildDeps.clear(); 910 } 911 _clearDuringReconcile.clear(); 912 913 assert checkIntegrity(); 914 return toDiscard; 915 } 916 917 /** 918 * Compare <code>contents</code> with the contents of <code>file</code>. 919 * @param contents the text to compare with the file's contents on disk. 920 * @param file does not have to exist. 921 * @return true if the file on disk cannot be read, or if its contents differ. 922 */ compareFileContents(String contents, IFile file)923 private boolean compareFileContents(String contents, IFile file) 924 { 925 boolean contentsDiffer = true; 926 if (file.exists()) { 927 InputStream oldData = null; 928 InputStream is = null; 929 try { 930 is = new ByteArrayInputStream(contents.getBytes()); 931 oldData = new BufferedInputStream(file.getContents()); 932 contentsDiffer = !FileSystemUtil.compareStreams(oldData, is); 933 } catch (CoreException ce) { 934 // Do nothing. Assume the new content is different 935 } finally { 936 if (oldData != null) { 937 try { 938 oldData.close(); 939 } catch (IOException ioe) { 940 } 941 } 942 if (is != null) { 943 try { 944 is.close(); 945 } catch (IOException ioe) { 946 } 947 } 948 } 949 } 950 return contentsDiffer; 951 } 952 953 /** 954 * Make the map updates necessary to discard build state. Typically called while 955 * processing a clean. In addition to throwing away the build dependencies, we also 956 * throw away all the blank working copies used to hide existing generated files, on 957 * the premise that since they were deleted in the clean we don't need to hide them 958 * any more. We leave the rest of the reconcile-time dependency info, though. 959 * <p> 960 * This method is atomic with regard to data structure integrity. This method 961 * does not touch disk nor create, discard, or modify compilation units. 962 * 963 * @return a list of working copies that the caller must discard by calling 964 * {@link CompilationUnitHelper#discardWorkingCopy(ICompilationUnit)}. 965 */ computeClean()966 private synchronized List<ICompilationUnit> computeClean() 967 { 968 _buildDeps.clearState(); 969 _clearDuringReconcile.clear(); 970 _reconcileNonDeps.clear(); 971 List<ICompilationUnit> toDiscard = new ArrayList<>(_hiddenBuiltTypes.values()); 972 _hiddenBuiltTypes.clear(); 973 974 assert checkIntegrity(); 975 return toDiscard; 976 } 977 978 /** 979 * Get the IFolder handles for any additional folders needed to 980 * contain a type in package <code>pkgName</code> under root 981 * <code>parent</code>. This does not actually create the folders 982 * on disk, it just gets resource handles. 983 * 984 * @return a set containing all the newly created folders. 985 */ computeNewPackageFolders(String pkgName, IFolder parent)986 private Set<IFolder> computeNewPackageFolders(String pkgName, IFolder parent) 987 { 988 Set<IFolder> newFolders = new HashSet<>(); 989 String[] folders = _PACKAGE_DELIMITER.split(pkgName); 990 for (String folderName : folders) { 991 final IFolder folder = parent.getFolder(folderName); 992 if (!folder.exists()) { 993 newFolders.add(folder); 994 } 995 parent = folder; 996 } 997 return newFolders; 998 } 999 1000 /** 1001 * Calculate the list of previously generated files that are no longer 1002 * being generated and thus need to be deleted. 1003 * <p> 1004 * This method does not touch the disk, nor does it create, update, or 1005 * discard working copies. This method is atomic with regard to the 1006 * integrity of data structures. 1007 * 1008 * @param parentFile only files solely parented by this file will be 1009 * added to the list to be deleted. 1010 * @param newlyGeneratedFiles files on this list will be spared. 1011 * @param toDiscard must be non-null. The caller should pass in an empty 1012 * list; on return the list will contain working copies which the caller 1013 * is responsible for discarding. 1014 * @param toReport must be non-null. The caller should pass in an empty 1015 * set; on return the set will contain IFiles representing source files 1016 * (but not non-source files such as text or xml files) which are being 1017 * deleted and which should therefore be removed from compilation. 1018 * @return a list of files which the caller should delete, ie by calling 1019 * {@link #deletePhysicalFile(IFile)}. 1020 */ computeObsoleteFiles( IFile parentFile, Set<IFile> newlyGeneratedFiles, List<ICompilationUnit> toDiscard, Set<IFile> toReport)1021 private synchronized Set<IFile> computeObsoleteFiles( 1022 IFile parentFile, Set<IFile> newlyGeneratedFiles, 1023 List<ICompilationUnit> toDiscard, 1024 Set<IFile> toReport) 1025 { 1026 Set<IFile> deleted = new HashSet<>(); 1027 Set<IFile> obsoleteFiles = _buildDeps.getValues(parentFile); 1028 // spare all the newly generated files 1029 obsoleteFiles.removeAll(newlyGeneratedFiles); 1030 for (IFile generatedFile : obsoleteFiles) { 1031 boolean isSource = _buildDeps.isSource(generatedFile); 1032 _buildDeps.remove(parentFile, generatedFile); 1033 if (AptPlugin.DEBUG_GFM_MAPS) AptPlugin.trace( 1034 "removed build file dependency: " + parentFile + " -> " + generatedFile); //$NON-NLS-1$ //$NON-NLS-2$ 1035 // If the file is still parented by any other parent, spare it 1036 if (!_buildDeps.containsValue(generatedFile)) { 1037 deleted.add(generatedFile); 1038 if (isSource) { 1039 toReport.add(generatedFile); 1040 } 1041 } 1042 } 1043 _clearDuringReconcile.removeAll(deleted); 1044 toDiscard.addAll(computeObsoleteHiddenTypes(parentFile, deleted)); 1045 assert checkIntegrity(); 1046 return deleted; 1047 } 1048 1049 /** 1050 * Calculate what needs to happen to working copies after a reconcile in order to get 1051 * rid of any no-longer-generated files. If there's an existing generated file, we 1052 * need to hide it with a blank working copy; if there's no existing file, we need to 1053 * get rid of any generated working copy. 1054 * <p> 1055 * A case to keep in mind: the user imports a project with already-existing generated 1056 * files, but without a serialized build dependency map. Then they edit a parent 1057 * file, causing a generated type to disappear. We need to discover and hide the 1058 * generated file on disk, even though it is not in the build-time dependency map. 1059 * 1060 * @param parentFile 1061 * the parent type being reconciled, which need not exist on disk. 1062 * @param newlyGeneratedFiles 1063 * the set of files generated in the last reconcile 1064 * @param toSetBlank 1065 * a list, to which this will add files that the caller must then set blank 1066 * with {@link CompilationUnitHelper#updateWorkingCopyContents(String, 1067 * ICompilationUnit, WorkingCopyOwner, boolean)} 1068 * @param toDiscard 1069 * a list, to which this will add files that the caller must then discard 1070 * with {@link CompilationUnitHelper#discardWorkingCopy(ICompilationUnit)}. 1071 */ computeObsoleteReconcileTypes( IFile parentFile, Set<IFile> newlyGeneratedFiles, CompilationUnitHelper cuh, List<ICompilationUnit> toSetBlank, List<ICompilationUnit> toDiscard)1072 private synchronized void computeObsoleteReconcileTypes( 1073 IFile parentFile, Set<IFile> newlyGeneratedFiles, 1074 CompilationUnitHelper cuh, 1075 List<ICompilationUnit> toSetBlank, List<ICompilationUnit> toDiscard) 1076 { 1077 // Get types previously but no longer generated during reconcile 1078 Set<IFile> obsoleteFiles = _reconcileDeps.getValues(parentFile); 1079 Map<IFile, ICompilationUnit> typesToDiscard = new HashMap<>(); 1080 obsoleteFiles.removeAll(newlyGeneratedFiles); 1081 for (IFile obsoleteFile : obsoleteFiles) { 1082 _reconcileDeps.remove(parentFile, obsoleteFile); 1083 if (_reconcileDeps.getKeys(obsoleteFile).isEmpty()) { 1084 ICompilationUnit wc = _reconcileGenTypes.remove(obsoleteFile); 1085 assert wc != null : 1086 "Value in reconcile deps missing from reconcile type list: " + obsoleteFile; //$NON-NLS-1$ 1087 typesToDiscard.put(obsoleteFile, wc); 1088 } 1089 } 1090 1091 Set<IFile> builtChildren = _buildDeps.getValues(parentFile); 1092 builtChildren.retainAll(_clearDuringReconcile); 1093 builtChildren.removeAll(newlyGeneratedFiles); 1094 for (IFile builtChild : builtChildren) { 1095 _reconcileNonDeps.put(parentFile, builtChild); 1096 // If it's on typesToDiscard there are no other reconcile-time parents. 1097 // If there are no other parents that are not masked by a nonDep entry... 1098 boolean foundOtherParent = false; 1099 Set<IFile> parents = _buildDeps.getKeys(builtChild); 1100 parents.remove(parentFile); 1101 for (IFile otherParent : parents) { 1102 if (!_reconcileNonDeps.containsKeyValuePair(otherParent, builtChild)) { 1103 foundOtherParent = true; 1104 break; 1105 } 1106 } 1107 if (!foundOtherParent) { 1108 ICompilationUnit wc = typesToDiscard.remove(builtChild); 1109 if (wc == null) { 1110 IPackageFragmentRoot root = _generatedPackageFragmentRoot.get().root; 1111 String typeName = getTypeNameForDerivedFile(builtChild); 1112 wc = cuh.getWorkingCopy(typeName, root); 1113 } 1114 _hiddenBuiltTypes.put(builtChild, wc); 1115 toSetBlank.add(wc); 1116 } 1117 } 1118 1119 // discard any working copies that we're not setting blank 1120 toDiscard.addAll(typesToDiscard.values()); 1121 1122 assert checkIntegrity(); 1123 } 1124 1125 /** 1126 * Calculate the list of blank working copies that are no longer needed because the 1127 * files that they hide have been deleted during a build. Remove these working copies 1128 * from the _hiddenBuiltTypes list and return them in a list. The caller MUST then 1129 * discard the contents of the list (outside of any synchronized block) by calling 1130 * CompilationUnitHelper.discardWorkingCopy(). 1131 * <p> 1132 * This method does not touch the disk and does not create, update, or discard working 1133 * copies. This method is atomic with regard to data structure integrity. 1134 * 1135 * @param parentFile 1136 * used to be a parent but may no longer be. 1137 * @param deletedFiles 1138 * a list of files which are being deleted, which might or might not have 1139 * been hidden by blank working copies. 1140 * 1141 * @return a list of working copies which the caller must discard 1142 */ computeObsoleteHiddenTypes(IFile parentFile, Set<IFile> deletedFiles)1143 private synchronized List<ICompilationUnit> computeObsoleteHiddenTypes(IFile parentFile, Set<IFile> deletedFiles) 1144 { 1145 List<ICompilationUnit> toDiscard = new ArrayList<>(); 1146 for (IFile deletedFile : deletedFiles) { 1147 if (_reconcileNonDeps.remove(parentFile, deletedFile)) { 1148 ICompilationUnit wc = _hiddenBuiltTypes.remove(deletedFile); 1149 if (wc != null) { 1150 toDiscard.add(wc); 1151 } 1152 } 1153 } 1154 assert checkIntegrity(); 1155 return toDiscard; 1156 } 1157 1158 /** 1159 * Delete a generated file from disk. Also deletes the parent folder hierarchy, up to 1160 * but not including the root generated source folder, as long as the folders are 1161 * empty and are marked as "derived". 1162 * <p> 1163 * This does not affect or refer to the dependency maps. 1164 * 1165 * @param file is assumed to be under the generated source folder. 1166 */ deletePhysicalFile(IFile file)1167 private void deletePhysicalFile(IFile file) 1168 { 1169 final IFolder genFolder = _gsfm.getFolder(); 1170 assert genFolder != null : "Generated folder == null"; //$NON-NLS-1$ 1171 IContainer parent = file.getParent(); // parent in the folder sense, 1172 // not the typegen sense 1173 try { 1174 if (AptPlugin.DEBUG_GFM) AptPlugin.trace( 1175 "delete physical file: " + file); //$NON-NLS-1$ 1176 file.delete(true, true, /* progressMonitor */null); 1177 } catch (CoreException e) { 1178 // File was locked or read-only 1179 AptPlugin.logWarning(e, "Unable to delete generated file: " + file); //$NON-NLS-1$ 1180 } 1181 // Delete the parent folders 1182 while (!genFolder.equals(parent) && parent != null && parent.isDerived()) { 1183 IResource[] members = null; 1184 try { 1185 members = parent.members(); 1186 } catch (CoreException e) { 1187 AptPlugin.logWarning(e, "Unable to read contents of generated file folder " + parent); //$NON-NLS-1$ 1188 } 1189 IContainer grandParent = parent.getParent(); 1190 // last one turns the light off. 1191 if (members == null || members.length == 0) 1192 try { 1193 parent.delete(true, /* progressMonitor */null); 1194 } catch (CoreException e) { 1195 AptPlugin.logWarning(e, "Unable to delete generated file folder " + parent); //$NON-NLS-1$ 1196 } 1197 else 1198 break; 1199 parent = grandParent; 1200 } 1201 } 1202 1203 /** 1204 * Given a typename a.b.C, this will return the IFile for the type name, where the 1205 * IFile is in the GENERATED_SOURCE_FOLDER_NAME. 1206 * <p> 1207 * This does not affect or refer to the dependency maps. 1208 */ getIFileForTypeName(String typeName)1209 public IFile getIFileForTypeName(String typeName) 1210 { 1211 // split the type name into its parts 1212 String[] parts = _PACKAGE_DELIMITER.split(typeName); 1213 1214 IFolder folder = _gsfm.getFolder(); 1215 for (int i = 0; i < parts.length - 1; i++) 1216 folder = folder.getFolder(parts[i]); 1217 1218 // the last part of the type name is the file name 1219 String fileName = parts[parts.length - 1] + ".java"; //$NON-NLS-1$ 1220 IFile file = folder.getFile(fileName); 1221 return file; 1222 } 1223 1224 /** 1225 * given file f, return the typename corresponding to the file. This assumes 1226 * that derived files use java naming rules (i.e., type "a.b.C" will be file 1227 * "a/b/C.java". 1228 */ getTypeNameForDerivedFile( IFile f )1229 private String getTypeNameForDerivedFile( IFile f ) 1230 { 1231 IPath p = f.getFullPath(); 1232 1233 IFolder folder = _gsfm.getFolder(); 1234 IPath generatedSourcePath = folder.getFullPath(); 1235 1236 int count = p.matchingFirstSegments( generatedSourcePath ); 1237 p = p.removeFirstSegments( count ); 1238 1239 String s = p.toPortableString(); 1240 int idx = s.lastIndexOf( '.' ); 1241 s = p.toPortableString().replace( '/', '.' ); 1242 return s.substring( 0, idx ); 1243 } 1244 1245 /** 1246 * Get a working copy for the specified generated type. If we already have 1247 * one cached, use that; if not, create a new one. Update the reconcile-time 1248 * dependency maps. 1249 * <p> 1250 * This method does not touch disk, nor does it update or discard any working 1251 * copies. However, it may call CompilationUnitHelper to get a new working copy. 1252 * This method is atomic with respect to data structures. 1253 * 1254 * @param parentFile the IFile whose processing is causing the new type to be generated 1255 * @param typeName the name of the type to be generated 1256 * @param cuh the CompilationUnitHelper utility object 1257 * @return a working copy ready to be updated with the new type's contents 1258 */ getWorkingCopyForReconcile(IFile parentFile, String typeName, CompilationUnitHelper cuh)1259 private synchronized ICompilationUnit getWorkingCopyForReconcile(IFile parentFile, String typeName, CompilationUnitHelper cuh) 1260 { 1261 IPackageFragmentRoot root = _generatedPackageFragmentRoot.get().root; 1262 IFile generatedFile = getIFileForTypeName(typeName); 1263 ICompilationUnit workingCopy; 1264 1265 workingCopy = _hiddenBuiltTypes.remove(generatedFile); 1266 if (null != workingCopy) { 1267 // file is currently hidden with a blank WC. Move that WC to the regular list. 1268 _reconcileNonDeps.remove(parentFile, generatedFile); 1269 _reconcileGenTypes.put(generatedFile, workingCopy); 1270 _reconcileDeps.put(parentFile, generatedFile); 1271 if (AptPlugin.DEBUG_GFM_MAPS) AptPlugin.trace( 1272 "moved working copy from hidden to regular list: " + generatedFile); //$NON-NLS-1$ 1273 } else { 1274 workingCopy = _reconcileGenTypes.get(generatedFile); 1275 if (null != workingCopy) { 1276 if (AptPlugin.DEBUG_GFM_MAPS) AptPlugin.trace( 1277 "obtained existing working copy from regular list: " + generatedFile); //$NON-NLS-1$ 1278 } else { 1279 // we've not yet created a working copy for this file, so make one now. 1280 workingCopy = cuh.getWorkingCopy(typeName, root); 1281 _reconcileDeps.put(parentFile, generatedFile); 1282 _reconcileGenTypes.put(generatedFile, workingCopy); 1283 if (AptPlugin.DEBUG_GFM_MAPS) AptPlugin.trace( 1284 "added new working copy to regular list: " + generatedFile); //$NON-NLS-1$ 1285 } 1286 } 1287 1288 assert checkIntegrity(); 1289 return workingCopy; 1290 } 1291 1292 /** 1293 * Check whether a child file has any parents that could apply in reconcile. 1294 * 1295 * @return true if <code>child</code> has no other parents in 1296 * {@link #_reconcileDeps}, and also no other parents in {@link #_buildDeps} 1297 * that are not masked by a corresponding entry in {@link #_reconcileNonDeps}. 1298 */ hasNoOtherReconcileParents(IFile child, IFile parent)1299 private boolean hasNoOtherReconcileParents(IFile child, IFile parent) { 1300 if (_reconcileDeps.valueHasOtherKeys(child, parent)) 1301 return true; 1302 Set<IFile> buildParents = _buildDeps.getKeys(child); 1303 buildParents.remove(parent); 1304 buildParents.removeAll(_reconcileNonDeps.getKeys(child)); 1305 return buildParents.isEmpty(); 1306 } 1307 1308 /** 1309 * Log extra file pairs, with a message like "message p1->g1, p2->g2". 1310 * Assumes that pairs has at least one entry. 1311 */ logExtraFilePairs(String message, Map<IFile, IFile> pairs)1312 private void logExtraFilePairs(String message, Map<IFile, IFile> pairs) { 1313 StringBuilder sb = new StringBuilder(); 1314 sb.append(message); 1315 Iterator<Map.Entry<IFile, IFile>> iter = pairs.entrySet().iterator(); 1316 while (true) { 1317 Map.Entry<IFile, IFile> entry = iter.next(); 1318 sb.append(entry.getKey().getName()); 1319 sb.append("->"); //$NON-NLS-1$ 1320 sb.append(entry.getValue().getName()); 1321 if (!iter.hasNext()) { 1322 break; 1323 } 1324 sb.append(", "); //$NON-NLS-1$ 1325 } 1326 String s = sb.toString(); 1327 AptPlugin.log(new IllegalStateException(s), s); 1328 } 1329 1330 /** 1331 * Log extra files, with a message like "message file1, file2, file3". 1332 * Assumes that files has at least one entry. 1333 */ logExtraFiles(String message, Iterable<IFile> files)1334 private void logExtraFiles(String message, Iterable<IFile> files) { 1335 StringBuilder sb = new StringBuilder(); 1336 sb.append(message); 1337 Iterator<IFile> iter = files.iterator(); 1338 while (true) { 1339 sb.append(iter.next().getName()); 1340 if (!iter.hasNext()) { 1341 break; 1342 } 1343 sb.append(", "); //$NON-NLS-1$ 1344 } 1345 String s = sb.toString(); 1346 AptPlugin.log(new IllegalStateException(s), s); 1347 } 1348 1349 /** 1350 * Given a fully qualified type name, generate the package name and the local filename 1351 * including the extension. For instance, type name <code>foo.bar.Baz</code> is 1352 * turned into package <code>foo.bar</code> and filename <code>Baz.java</code>. 1353 * <p> 1354 * TODO: this is almost identical to code in CompilationUnitHelper. Is the difference 1355 * intentional? 1356 * 1357 * @param qualifiedName 1358 * a fully qualified type name 1359 * @return a String array containing {package name, filename} 1360 */ parseTypeName(String qualifiedName)1361 private static String[] parseTypeName(String qualifiedName) { 1362 1363 //TODO: the code in CompilationUnitHelper doesn't perform this check. Should it? 1364 if (qualifiedName.indexOf('/') != -1) 1365 qualifiedName = qualifiedName.replace('/', '.'); 1366 1367 String[] names = new String[2]; 1368 String pkgName; 1369 String fname; 1370 int idx = qualifiedName.lastIndexOf( '.' ); 1371 if ( idx > 0 ) 1372 { 1373 pkgName = qualifiedName.substring( 0, idx ); 1374 fname = 1375 qualifiedName.substring(idx + 1, qualifiedName.length()) + ".java"; //$NON-NLS-1$ 1376 } 1377 else 1378 { 1379 pkgName = ""; //$NON-NLS-1$ 1380 fname = qualifiedName + ".java"; //$NON-NLS-1$ 1381 } 1382 names[0] = pkgName; 1383 names[1] = fname; 1384 return names; 1385 } 1386 1387 /** 1388 * Remove a file from the build-time dependency maps, and calculate the consequences 1389 * of the removal. This is called in response to a file being deleted by the 1390 * environment. 1391 * <p> 1392 * This operation affects the maps only. This operation is atomic with respect to map 1393 * integrity. This operation does not touch the disk nor create, update, or discard 1394 * any working copies. 1395 * 1396 * @param f 1397 * can be a parent, generated, both, or neither. 1398 * @return a list of generated files that are no longer relevant and must be deleted. 1399 * This operation must be done by the caller without holding any locks. The 1400 * list may be empty but will not be null. 1401 */ removeFileFromBuildMaps(IFile f)1402 private synchronized List<IFile> removeFileFromBuildMaps(IFile f) 1403 { 1404 List<IFile> toDelete = new ArrayList<>(); 1405 // Is this file the sole parent of files generated during build? 1406 // If so, add them to the deletion list. Then remove the file from 1407 // the build dependency list. 1408 Set<IFile> childFiles = _buildDeps.getValues(f); 1409 for (IFile childFile : childFiles) { 1410 Set<IFile> parentFiles = _buildDeps.getKeys(childFile); 1411 if (parentFiles.size() == 1 && parentFiles.contains(f)) { 1412 toDelete.add(childFile); 1413 } 1414 } 1415 boolean removed = _buildDeps.removeKey(f); 1416 if (removed) { 1417 if (AptPlugin.DEBUG_GFM_MAPS) AptPlugin.trace( 1418 "removed parent file from build dependencies: " + f); //$NON-NLS-1$ 1419 } 1420 1421 assert checkIntegrity(); 1422 return toDelete; 1423 } 1424 1425 /** 1426 * Remove the generated children of a working copy from the reconcile dependency maps. 1427 * Typically invoked when a working copy of a parent file has been discarded by the 1428 * editor; in this case we want to remove any generated working copies that it 1429 * parented. 1430 * <p> 1431 * This method does not touch disk nor create, modify, or discard working copies. This 1432 * method is atomic with regard to data structure integrity. 1433 * 1434 * @param file 1435 * a file representing a working copy that is not necessarily a parent or 1436 * generated file 1437 * @return a list of generated working copies that are no longer referenced and should 1438 * be discarded by calling 1439 * {@link CompilationUnitHelper#discardWorkingCopy(ICompilationUnit)} 1440 */ removeFileFromReconcileMaps(IFile file)1441 private synchronized List<ICompilationUnit> removeFileFromReconcileMaps(IFile file) 1442 { 1443 List<ICompilationUnit> toDiscard = new ArrayList<>(); 1444 // remove all the orphaned children 1445 Set<IFile> genFiles = _reconcileDeps.getValues(file); 1446 for (IFile child : genFiles) { 1447 if (hasNoOtherReconcileParents(child, file)) { 1448 ICompilationUnit childWC = _reconcileGenTypes.remove(child); 1449 assert null != childWC : "Every value in _reconcileDeps must be a key in _reconcileGenTypes"; //$NON-NLS-1$ 1450 toDiscard.add(childWC); 1451 } 1452 } 1453 _reconcileDeps.removeKey(file); 1454 1455 // remove obsolete entries in non-generated list 1456 Set<IFile> nonGenFiles = _reconcileNonDeps.getValues(file); 1457 for (IFile child : nonGenFiles) { 1458 ICompilationUnit hidingWC = _hiddenBuiltTypes.remove(child); 1459 if (null != hidingWC) { 1460 toDiscard.add(hidingWC); 1461 } 1462 } 1463 _reconcileNonDeps.removeKey(file); 1464 1465 assert checkIntegrity(); 1466 return toDiscard; 1467 } 1468 1469 /** 1470 * Write <code>contents</code> to disk in the form of a compilation unit named 1471 * <code>name</code> under package fragment <code>pkgFrag</code>. The way in 1472 * which the write is done depends whether the compilation unit is a working copy. 1473 * <p> 1474 * The working copy is used in reconcile. In principle changing the contents during 1475 * build should be a problem, since the Java builder is based on file contents rather 1476 * than on the current Java Model. However, annotation processors get their type info 1477 * from the Java Model even during build, so there is in general no difference between 1478 * build and reconcile. This causes certain bugs (if a build is performed while there 1479 * is unsaved content in editors), so it may change in the future, and this routine 1480 * will need to be fixed. - WHarley 11/06 1481 * <p> 1482 * This method touches the disk and modifies working copies. It can only be called 1483 * during build, not during reconcile, and it should not be called while holding any 1484 * locks (other than the workspace rules held by the build). 1485 * 1486 * @param pkgFrag 1487 * the package fragment in which the type will be created. The fragment's 1488 * folders must already exist on disk. 1489 * @param cuName 1490 * the simple name of the type, with extension, such as 'Obj.java' 1491 * @param contents 1492 * the text of the compilation unit 1493 * @param progressMonitor 1494 */ saveCompilationUnit(IPackageFragment pkgFrag, final String cuName, String contents, IProgressMonitor progressMonitor)1495 private void saveCompilationUnit(IPackageFragment pkgFrag, final String cuName, String contents, 1496 IProgressMonitor progressMonitor) 1497 { 1498 1499 ICompilationUnit unit = pkgFrag.getCompilationUnit(cuName); 1500 boolean isWorkingCopy = unit.isWorkingCopy(); 1501 if (isWorkingCopy) { 1502 try { 1503 // If we have a working copy, all we 1504 // need to do is update its contents and commit it... 1505 _CUHELPER.commitNewContents(unit, contents, progressMonitor); 1506 if (AptPlugin.DEBUG_GFM) AptPlugin.trace( 1507 "Committed existing working copy during build: " + unit.getElementName()); //$NON-NLS-1$ 1508 } 1509 catch (JavaModelException e) { 1510 // ...unless, that is, the resource has been deleted behind our back 1511 // due to a clean. In that case, discard the working copy and try again. 1512 if (e.getJavaModelStatus().getCode() == IJavaModelStatusConstants.INVALID_RESOURCE) { 1513 _CUHELPER.discardWorkingCopy(unit); 1514 isWorkingCopy = false; 1515 if (AptPlugin.DEBUG_GFM) AptPlugin.trace( 1516 "Discarded invalid existing working copy in order to try again: " + unit.getElementName()); //$NON-NLS-1$ 1517 } 1518 else { 1519 AptPlugin.log(e, "Unable to commit working copy to disk: " + unit.getElementName()); //$NON-NLS-1$ 1520 return; 1521 } 1522 } 1523 } 1524 if (!isWorkingCopy) { 1525 try { 1526 unit = pkgFrag.createCompilationUnit(cuName, contents, true, progressMonitor); 1527 if (AptPlugin.DEBUG_GFM) AptPlugin.trace( 1528 "Created compilation unit during build: " + unit.getElementName()); //$NON-NLS-1$ 1529 } catch (JavaModelException e) { 1530 AptPlugin.log(e, "Unable to create compilation unit on disk: " + //$NON-NLS-1$ 1531 cuName + " in pkg fragment: " + pkgFrag.getElementName()); //$NON-NLS-1$ 1532 } 1533 } 1534 } 1535 1536 } 1537