blob: 8aba5a768e2be1a410d717838c01fb3fd1074b38 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}