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