/******************************************************************************* * Copyright (c) 2005, 2012 BEA Systems, Inc. and others * 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 * IBM Corporation - modified to fix https://bugs.eclipse.org/bugs/show_bug.cgi?id=185601 *******************************************************************************/ 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> * We attempt to update the classpath with an entry for the generated source * folder if APT is enabled and there is no entry for this folder present * on the classpath. * <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. * * Classpath will be updated with an entry for the generated source folder * if it is not already added to the classpath. See bug 185601. */ 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 (addToClasspath(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; } }