Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 8aba5a768e2be1a410d717838c01fb3fd1074b38 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
/*******************************************************************************
 * Copyright (c) 2005, 2007 BEA Systems, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   wharley@bea.com - initial API and implementation
 *******************************************************************************/

package org.eclipse.jdt.apt.core.internal.generatedfile;

import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.apt.core.internal.AptPlugin;
import org.eclipse.jdt.apt.core.internal.AptProject;
import org.eclipse.jdt.apt.core.internal.util.FileSystemUtil;
import org.eclipse.jdt.apt.core.util.AptConfig;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaModelException;

/**
 * Manage the generated source folder for an APT project.
 * Every AptProject has a GeneratedSourceFolderManager.  Depending on whether APT
 * is enabled for the project, there may or may not be an actual generated
 * source folder on disk; GeneratedSourceFolderManager is responsible for creating
 * and deleting this folder as needed whenever APT settings are changed.
 * <p>
 * The job of the GeneratedSourceFolderManager is to keep the following data
 * in agreement: 
 * <ul>
 * <li>whether APT is enabled</li>
 * <li>the name of the generated source folder</li>
 * <li>the existence of the actual folder on disk</li>
 * <li>the presence of a classpath entry for the folder</li>
 * <li>problem markers indicating a disagreement in any of the above</li>
 * </ul>
 * We attempt to change the classpath entry and the folder on disk whenever
 * the enabled/disabled state or the folder name change.  These changes are
 * discovered via the preferenceChanged() method. 
 * <p>
 * GeneratedSourceFolderManager is responsible only for the folder itself, not
 * its contents.  Contents are managed by @see GeneratedFileManager.
 *  
 */
public class GeneratedSourceFolderManager {
	
	private final AptProject _aptProject;

	/**
	 * The folder where generated source files are placed.  This will be
	 * null if APT is disabled, or in any other error state (e.g., folder
	 * does not exist on disk; folder exists on disk but classpath entry
	 * does not exist).
	 * <p>
	 * In general, if we see that this member is null but the ENABLED
	 * preference is true, we will try to create the folder and add it to
	 * the classpath; if we see that this member is non-null but the
	 * ENABLED preference is false, we will try to delete this folder's
	 * contents and remove it from the classpath; and if we see that the
	 * ENABLED preference is true, but the GENSRCDIR folder name preference
	 * is different than the name of this folder, we will try to delete
	 * this folder's contents, remove it from the classpath, and create a
	 * new folder and add it to the classpath.  When we do this work depends
	 * on when we get notified of relevant changes and on what locks we are
	 * able to obtain.
	 */
	private IFolder _generatedSourceFolder = null;
	
	/**
	 * Should be constructed only by AptProject.  Other clients should call
	 * @see AptProject#getGeneratedSourceFolderManager() to get this object.
	 */
	public GeneratedSourceFolderManager(AptProject aptProject) 
	{
		_aptProject = aptProject;
		final IJavaProject javaProject = aptProject.getJavaProject();
		
		// Set _generatedSourceFolder only if APT is enabled, the folder exists,
		// and the folder is on the classpath.  
		// Otherwise leave it null, which will cause us to try to fix things later on.
		if (AptConfig.isEnabled(javaProject)) {
			final IFolder folder = getFolder();
			if (folder.exists()) {
				if (isOnClasspath(folder)) {
					_generatedSourceFolder = folder;
				}
			}
		}
	}
	
	/**
	 * Add the folder to the classpath, unless it's already there.
	 * @param srcFolder the folder to add to the classpath.  Must not be null.
	 * @return true if, at the end of the routine, the folder is on the classpath.
	 */
	private boolean addToClasspath(IFolder srcFolder) {
		boolean onClasspath = false;
		try {
			ClasspathUtil.updateProjectClasspath( _aptProject.getJavaProject(), srcFolder, null );
			if(AptPlugin.DEBUG)
				AptPlugin.trace("Ensured classpath has an entry for " + srcFolder); //$NON-NLS-1$
			onClasspath = true;
		}
		catch (CoreException e) {						
			e.printStackTrace();
			AptPlugin.log(e, "Failed to add classpath entry for generated source folder " + srcFolder.getName()); //$NON-NLS-1$
		}
		return onClasspath;
	}
	
	/**
	 * Call this to create the folder and add it to the classpath, when APT is enabled
	 * (in which case the folder did not previously exist) or when the folder name is
	 * changed (in which case the old stuff must also be removed).  
	 * <p>
	 * This method will take a resource lock if the generated source folder needs 
	 * to be created on disk, and it will take a java model lock if the project's 
	 * source paths need to be updated.  Care should be taken when calling this 
	 * method to ensure that locking behavior is correct.
	 * <p>
 	 * This should only be called on an event thread, with no locks on the project
	 * or classpath.
	 */
	private void configure() {
		
		assert(_generatedSourceFolder == null): "Should have already removed old folder by now"; //$NON-NLS-1$
		IFolder srcFolder = getFolderPreference();
		if (srcFolder == null) {
			IStatus status = AptPlugin.createStatus(null, "Could not create generated source folder (" + //$NON-NLS-1$
					AptConfig.getGenSrcDir(_aptProject.getJavaProject()) + ")"); //$NON-NLS-1$
			AptPlugin.log(status);
			return;
		}
		
		// Ensure that the new folder exists on disk.
		if (createOnDisk(srcFolder)) {
			// Add it to the classpath.
			if (addToClasspath(srcFolder)) {
				// Only if we get this far do we actually set _generatedSourceFolder.
				synchronized ( this ) {
					_generatedSourceFolder = srcFolder;
				}
			}
		}
	}
	
	
	/**
	 * Creates the generated source folder if necessary.  This should be called just
	 * before doing a build.
	 * No changes to the classpath will be made.
	 */
	public void ensureFolderExists(){
		// If APT is disabled, do nothing.
		if (!AptConfig.isEnabled(_aptProject.getJavaProject())) {
			return;
		}
		
		// In principle we could bail out here, if (_generatedSourceFolder != null).
		// However, this method is an opportunity to detect and fix problems such 
		// as the folder getting deleted without generatedSourceFolderDeleted() 
		// getting called (e.g., without user having done a refresh).
		IFolder srcFolder = getFolder();
		if (srcFolder == null) {
			IStatus status = AptPlugin.createStatus(null, "Could not create generated source folder (" + //$NON-NLS-1$
					AptConfig.getGenSrcDir(_aptProject.getJavaProject()) + ")"); //$NON-NLS-1$
			AptPlugin.log(status);
			return;
		}
		
		if (createOnDisk(srcFolder)) {
			if (isOnClasspath(srcFolder)) {
				synchronized (this) {
					// Only set _generatedSourceFolder if folder is on disk and on classpath.
					_generatedSourceFolder = srcFolder;
				}
			}
		}
	}

	/**
	 * Create a folder on disk, unless it already exists.
	 * <p>
	 * This method will frequently be called on multiple threads simultaneously
	 * (e.g., build thread and UI thread).
	 * @param srcFolder the folder to create.  Must not be null.
	 * @return true if, at the end of the routine, the folder exists on disk.
	 */
	private boolean createOnDisk(IFolder srcFolder) {
		boolean exists = false;
		try {
			// don't take any locks while creating the folder, since we are doing file-system operations
			srcFolder.refreshLocal( IResource.DEPTH_INFINITE, null );
			if (!srcFolder.exists()) {
				FileSystemUtil.makeDerivedParentFolders(srcFolder);
				if(AptPlugin.DEBUG)
					AptPlugin.trace("Created folder " + srcFolder + " on disk"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			exists = true;
		}
		catch (CoreException e) {						
			e.printStackTrace();
			AptPlugin.log(e, "Failed to ensure existence of generated source folder " + srcFolder.getName()); //$NON-NLS-1$
		}
		return exists;
	}

	/**
	 * Call this method when the APT_ENABLED preference has changed.
	 * 
	 * Configure the generated source folder according to whether APT is enabled
	 * or disabled.  If enabled, the folder will be created and a classpath entry
	 * will be added.  If disabled, the folder and classpath entry will be removed.
	 * <p>
	 * This should only be called on an event thread, with no locks on the project
	 * or classpath.
	 */
	public void enabledPreferenceChanged()
	{
		final boolean enable = AptConfig.isEnabled(_aptProject.getJavaProject());
		// Short-circuit if nothing changed.
		if (enable == (_generatedSourceFolder != null)) {
			if( AptPlugin.DEBUG ) {
				AptPlugin.trace("enabledChanged() doing nothing; state is already " + enable); //$NON-NLS-1$
			}
			// no change in state
			return;
		}
		
		if ( AptPlugin.DEBUG ) {
			AptPlugin.trace("enabledChanged() changing state to " + enable +  //$NON-NLS-1$
					" for " + _aptProject.getJavaProject().getElementName()); //$NON-NLS-1$
		}
		if( enable ) {
			configure();
		} 
		else {
			removeFolder();
		}
	}

	/**
	 * Respond to a change in the name of the generated source folder.  
	 * If APT is enabled, remove the old folder and classpath entry and 
	 * create new ones.
	 * <p>
 	 * This should only be called on an event thread, with no locks on the project
	 * or classpath.
	 */
	public void folderNamePreferenceChanged()
	{
		// if APT is disabled, we don't need to do anything
		final boolean aptEnabled = AptConfig.isEnabled(_aptProject.getJavaProject());
		if (!aptEnabled) {
			return;
		}
		
		// if name didn't change, we don't need to do anything
		if (_generatedSourceFolder != null && _generatedSourceFolder.equals(getFolderPreference())) {
			if( AptPlugin.DEBUG ) {
				AptPlugin.trace("folderNameChanged() doing nothing; name is already " +  //$NON-NLS-1$
						_generatedSourceFolder.getProjectRelativePath());
			}
			return;
		}
		
		removeFolder();
		configure();
	}
	
	/**
	 *  Invoked when the generated source folder has been deleted.  This will 
	 *  flush any in-memory state tracking generated files, and cause the
	 *  generated source folder to be recreated the next time we build.
	 *  
	 *  Note: this should only be called within a resource change event to ensure that the classpath
	 *  is correct during any build. Resource change event never occurs during a build.
	 */
	public void folderDeleted()
	{
		_aptProject.projectClean( false );
		
		IFolder srcFolder;
		synchronized(this){
			srcFolder = _generatedSourceFolder;
			_generatedSourceFolder = null;
		}
		if(AptPlugin.DEBUG)
			AptPlugin.trace("set _generatedSourceFolder to null; was " + srcFolder ); //$NON-NLS-1$
	}
	
	/**
	 * This method will return the binary output location for the generated source folder.
	 * If the generated-source folder is not configured (i.e., not created or not added to
	 * the project's source path, then this method will return the default binary output
	 * location for the project. 
	 *
	 * @return the IPath corresponding to the binary output location for the
	 * generated source folder. This is relative to the project.
	 * 
	 * @throws JavaModelException
	 * 
	 * @see #getFolder()
	 */
	public IPath getBinaryOutputLocation()
		 throws JavaModelException 
	{
		IPath outputRootPath = null;
		IFolder generatedSourceFolder = getFolder();
		if ( generatedSourceFolder != null && generatedSourceFolder.exists() )
		{
			IClasspathEntry cpe = ClasspathUtil.findProjectSourcePath( _aptProject.getJavaProject(), generatedSourceFolder );
			if ( cpe != null )
				outputRootPath = cpe.getOutputLocation();
		}
		
		// no output root, so get project's default output location
		if ( outputRootPath == null )
			outputRootPath = _aptProject.getJavaProject().getOutputLocation();

		// output location is relative to the workspace, we want to make it relative to project
		int segments = outputRootPath.matchingFirstSegments( _aptProject.getJavaProject().getPath() );
		outputRootPath = outputRootPath.removeFirstSegments( segments );
		
		return outputRootPath;
	}
	
	/**
	 * Get the current generated source folder; or if it is null, return
	 * an IFolder corresponding to the current generated source folder name.
	 * This is a handle-only operation and does not have anything to do with
	 * whether the folder exists on disk.
	 * @throws IllegalArgumentException if the name is invalid (e.g., "..").
	 */
	public IFolder getFolder(){
		
		synchronized (this) {
			if( _generatedSourceFolder != null )
				return _generatedSourceFolder;
		}
		
		return getFolderPreference();
	}

	/**
	 * Get an IFolder that corresponds to the folder name preference.
	 * This has nothing to do with whether APT is enabled or disabled,
	 * nothing to do with whether the folder exists on disk; it's just
	 * a handle corresponding to a name.
	 * @return null if the IFolder could not be created, which probably
	 * means that the name is something illegal like "..".
	 */
	private IFolder getFolderPreference() {
		final String folderName = AptConfig.getGenSrcDir(_aptProject.getJavaProject());
		IFolder folder = null;
		try {
			folder = _aptProject.getJavaProject().getProject().getFolder( folderName );
		}
		catch (IllegalArgumentException e) {
			// In the event that the folderName is invalid, just return null.
		}
		return folder;
	}
	
	/**
	 * returns true if the specified folder is the source folder used where
	 * generated files are placed. 
	 * 
	 * @param folder - the folder to determine if it is the generated source folder
	 * @return true if it is the generated source folder, false otherwise.  
	 * 
	 * @see #getFolder()
	 */
	public boolean isGeneratedSourceFolder( IFolder folder )
	{
		return folder != null && folder.equals( getFolder() );
	}

	private boolean isOnClasspath(IFolder srcFolder) {
		boolean found = false;
		try {
			if (ClasspathUtil.doesClasspathContainEntry(
					_aptProject.getJavaProject(), null, srcFolder.getFullPath(), null)) {
				found = true;
			}
		} catch (JavaModelException e) {
			e.printStackTrace();
		}
		return found;
	}

	/**
	 * Remove a folder from disk and from the classpath.
	 * @param srcFolder
	 */
	private void removeFolder() {
		final IFolder srcFolder;
		synchronized ( this )
		{
			srcFolder = _generatedSourceFolder;
			_generatedSourceFolder = null;
		}
		if (srcFolder == null) {
			return;
		}
		
		// Clear out the generated file maps
		_aptProject.projectClean(false);
		
		// clean up the classpath first so that when we actually delete the 
		// generated source folder we won't cause a classpath error.
		try {
			if (srcFolder.isDerived()) {
				ClasspathUtil.removeFromProjectClasspath( _aptProject.getJavaProject(), srcFolder, null );
			}
		} catch (JavaModelException e) {
			AptPlugin.log( e, "Failed to remove classpath entry for old generated src folder " + srcFolder.getName() ); //$NON-NLS-1$
		}
		
		final IWorkspaceRunnable runnable = new IWorkspaceRunnable(){
	        public void run(IProgressMonitor monitor)
	        {		
            	try {
            		IResource parent = srcFolder.getParent();
            		boolean deleted = FileSystemUtil.deleteDerivedResources(srcFolder);
            		
            		// We also want to delete our parent folder(s) if they are derived and empty
            		if (deleted) {
            			while (parent.isDerived() && parent.getType() == IResource.FOLDER) {
            				IFolder parentFolder = (IFolder)parent;
            				if (parentFolder.members().length == 0) {
            					parent = parentFolder.getParent();
            					FileSystemUtil.deleteDerivedResources(parentFolder);
            				}
            				else {
            					break;
            				}
            			}
            		}
            		
            	} catch(CoreException e) {
            		AptPlugin.log(e, "failed to delete old generated source folder " + srcFolder.getName() ); //$NON-NLS-1$
            	} catch(OperationCanceledException cancel) {
            		AptPlugin.log(cancel, "deletion of generated source folder got cancelled"); //$NON-NLS-1$
            	}
	        }
	    };
	    IWorkspace ws = ResourcesPlugin.getWorkspace();
	    try{
	    	ws.run(runnable, ws.getRoot(), IWorkspace.AVOID_UPDATE, null);
	    }catch(CoreException e){
			AptPlugin.log(e, "Runnable for deleting old generated source folder " + srcFolder.getName() + " failed."); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * Check whether the proposed name is permitted.
	 * @param folderName can be anything, including null.
	 * @return true if attempting to set the generated source folder to 
	 * <code>dirString</code> is likely to succeed.
	 */
	public static boolean validate(final IJavaProject jproj, final String folderName) {
		boolean succeeded = false;
		try {
			if (jproj != null) {
				// If we have a specific project, we can just ask.
				IFolder folder = null;
				folder = jproj.getProject().getFolder( folderName );
				succeeded = (folder != null);
			}
			else {
				// We're being asked about the default, so no specific project;
				// here we have to guess.  The code that will later fail if we
				// get it wrong is IProject.getFolder(String).  So we use some
				// heuristics.
				IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
				IPath state = AptPlugin.getPlugin().getStateLocation();
				IPath proposed = new Path(folderName);
				IPath combined = state.append(proposed);
				if (combined.segmentCount() <= state.segmentCount()) {
					// proposed folder depth is too shallow
					return false;
				}
				IFolder folder = root.getFolder(combined);
				succeeded = (folder != null);
			}
		}
		catch (IllegalArgumentException e) {
			return false;
		}
		return succeeded;
	}

}

Back to the top