/*******************************************************************************
* Copyright (c) 2014 Zend Technologies 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:
* Zend Technologies - initial API and implementation
*******************************************************************************/
package org.eclipse.php.core.libfolders;
import java.util.*;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.php.internal.core.PHPCorePlugin;
import org.eclipse.php.internal.core.libfolders.AutoDetectLibraryFolderListener;
import org.eclipse.wst.validation.ValidationFramework;
import org.eclipse.wst.validation.internal.DisabledResourceManager;
/**
* Provides the necessary API for working with library folders.
*
* <p>
* Library folders are source folders containing code, which is not
* application-specific. This code is not validated, but is still accessible for
* content assist.
* </p>
*
* <p>
* This class is a singleton. Use the {@link #getInstance()} to obtain an
* instance of the class.
* </p>
*
* @author Kaloyan Raev
* @since 3.3
*/
public class LibraryFolderManager {
/**
* The singleton's instance
*/
private static LibraryFolderManager instance;
/**
* The collection of registered library folder change listeners.
*/
private Collection<ILibraryFolderChangeListener> listeners;
private AutoDetectLibraryFolderListener listener;
/**
* Private constructor to initialize the library folder manager.
*
* <p>
* This constructor should not be called by clients. Use the
* {@link #getInstance()} to obtain an instance of the class.
* </p>
*
* <p>
* This constructor registers the {@link AutoDetectLibraryFolderListener} as
* a resource change listener.
* </p>
*/
private LibraryFolderManager() {
listener = new AutoDetectLibraryFolderListener();
listeners = Collections.synchronizedSet(new HashSet<ILibraryFolderChangeListener>());
ResourcesPlugin.getWorkspace().addResourceChangeListener(listener, IResourceChangeEvent.POST_CHANGE);
}
/**
* Returns the instance of the singleton class.
*
* @return an instance of <code>LibraryFolderManager</code>.
*/
public static synchronized LibraryFolderManager getInstance() {
if (instance == null) {
instance = new LibraryFolderManager();
}
return instance;
}
/**
* Adds the given listener for library folder change events. Has no effect
* if an identical listener is already registered.
*
* <p>
* The listener will be notified whenever a source folder is turned to
* library folder and vice versa.
* </p>
*
* @param listener
* the listener
*/
public void addListener(ILibraryFolderChangeListener listener) {
listeners.add(listener);
}
/**
* Removes the given library folder change listener from this workspace. Has
* no effect if an identical listener is not registered.
*
* @param listener
* the listener
*/
public void removeListener(ILibraryFolderChangeListener listener) {
listeners.remove(listener);
}
/**
* Marks the given source folders as library folders.
*
* <p>
* This method executes the following steps:
* <ol>
* <li>Disables the given folders in the WTP Validation Framework.</li>
* <li>Notifies the registered library folder change listeners, e.g. for
* updating the images of the given folders and all their subfolders in the
* PHP Explorer.</li>
* <li>Waits for any running validation job to finish.</li>
* <li>Deletes all problem and task markers on all the children resources of
* the given folders.</li>
* </ol>
* </p>
*
* <p>
* This method usually executes a significant number of resource
* modifications. It is highly recommended that this method is called either
* in a {@link WorkspaceModifyOperation} or in a {@link WorkspaceJob} to
* batch the resource change notifications and avoid running unnecessary
* build jobs.
* </p>
*
* @param folders
* an array of {@link IFolder} objects that represents the source
* folders to be marked as library folders
* @param monitor
* a progress monitor, or null if progress reporting is not
* desired
*
* @throws OperationCanceledException
* if the progress monitor is canceled while waiting
* @throws InterruptedException
* if this thread is interrupted while waiting
* @throws CoreException
* if this method fails for some other reason
*
* @see {@link #useAsSourceFolder(IFolder[], IProgressMonitor)}
*/
public void useAsLibraryFolder(IFolder[] folders, IProgressMonitor monitor)
throws OperationCanceledException, InterruptedException, CoreException {
disableValidation(folders);
folders = removeNonExisting(folders);
if (folders.length > 0) {
notifyListeners(folders);
waitValidationJobs(monitor);
deleteMarkers(folders);
}
}
/**
* Marks the given library folders as source folders.
*
* <p>
* This method executes the following steps:
* <ol>
* <li>Enables the given folders in the WTP Validation Framework.</li>
* <li>Notifies the registered library folder change listeners, e.g. for
* updating the images of the given folders and all their subfolders in the
* PHP Explorer.</li>
* <li>Waits for any running validation job to finish.</li>
* <li>Invokes {@link IResource#touch(IProgressMonitor)} on all the children
* of the selected folders. This triggers the necessary builders to
* re-validate these resources.</li>
* </ol>
* </p>
*
* <p>
* This method usually executes a significant number of resource
* modifications. It is highly recommended that this method is called either
* in a {@link WorkspaceModifyOperation} or in a {@link WorkspaceJob} to
* batch the resource change notifications and avoid running unnecessary
* build jobs.
* </p>
*
* @param folders
* an array of {@link IFolder} objects that represents the
* library folders to be marked as source folders
* @param monitor
* a progress monitor, or null if progress reporting is not
* desired
*
* @throws OperationCanceledException
* if the progress monitor is canceled while waiting
* @throws InterruptedException
* if this thread is interrupted while waiting
* @throws CoreException
* if this method fails for some other reason
*
* @see {@link #useAsLibraryFolder(IFolder[], IProgressMonitor)}
*/
public void useAsSourceFolder(IFolder[] folders, IProgressMonitor monitor)
throws OperationCanceledException, InterruptedException, CoreException {
enableValidation(folders);
folders = removeNonExisting(folders);
if (folders.length > 0) {
notifyListeners(folders);
waitValidationJobs(monitor);
revalidate(folders, monitor);
}
}
/**
* Enables validation for the given folder in the WTP Validation Framework.
*
* @param folder
* a folder to enable validation for
*/
public void enableValidation(IFolder folder) {
ValidationFramework vf = ValidationFramework.getDefault();
vf.enableValidation(folder);
}
/**
* Disables validation for the given folder in the WTP Validation Framework.
*
* <p>
* This method cleans up the disabled state for all subfolders of the given
* folder. This is necessary to avoid nested declarations of library
* folders. Otherwise, if the user marks a library folder as a source folder
* and there are nested library folders then there will be still subfolders
* which remain library folders.
* </p>
*
* @param folder
* a folder to disable validation for
*
* @throws CoreException
* if any of the folders does not exist or is in a closed
* project
*/
public void disableValidation(IFolder folder) throws CoreException {
ValidationFramework vf = ValidationFramework.getDefault();
// clean up the state of all subfolders
for (IFolder subfolder : getAllSubfolders(folder)) {
vf.enableValidation(subfolder);
}
if (!isInLibraryFolder(folder)) {
// disable the given folder only if no parent folder is a library
// folder yet
vf.disableValidation(folder);
}
}
/**
* Enables validation for the given folders in the WTP Validation Framework.
*
* <p>
* This method invokes {@link #enableValidation(IFolder)} for each of the
* folders in the given array.
* </p>
*
* @param folders
* an array of folders to enable validation for
*/
public void enableValidation(IFolder[] folders) {
for (IFolder folder : folders) {
enableValidation(folder);
}
}
/**
* Disables validation for the given folders in the WTP Validation
* Framework.
*
* <p>
* This method invokes {@link #disableValidation(IFolder)} for each of the
* folders in the given array.
* </p>
*
* @param folders
* an array of folders to disable validation for
*
* @throws CoreException
* if any of the folders does not exist or is in a closed
* project
*/
public void disableValidation(IFolder[] folders) throws CoreException {
for (IFolder folder : folders) {
disableValidation(folder);
}
}
/**
* Returns whether the given resource is inside a library folder.
*
* <p>
* A library folder is a source folder that is disabled for validation in
* the WTP Validation Framework. So, this method checks if a parent folder
* of the given resource is disabled for validation.
* </p>
*
* @param resource
* a resource to check if inside a library folder
*
* @return <code>true</code> if the given resource is inside a library
* folder, and <code>false</code> otherwise
*
* @see ValidationFramework#disableValidation(IResource)
*/
public boolean isInLibraryFolder(IResource resource) {
if (resource == null)
return false;
if (resource.getType() == IResource.FILE) {
// the resource is a source file, so take its parent folder
resource = resource.getParent();
}
while (resource.getType() == IResource.FOLDER) {
// check if the folder is disabled in the WTP Validation Framework
if (isExplicitlyDisabled((IFolder) resource)) {
return true;
}
// the folder is not disabled, so check its parent folder
resource = resource.getParent();
}
// none of the resource's parent folders is disabled in the WTP
// Validation Framework, so the resource is not inside a library folder
return false;
}
/**
* Returns whether the the given folder is a library folder that is
* explicitly disabled in the WTP Validation Framework.
*
* <p>
* This is a folder that was passed as parameter to the
* {@link #disableValidation(IFolder)} method.
* </p>
*
* @param folder
* a folder to check
*
* @return <code>true</code> if the folder is a explicitly disabled, and
* <code>false</code> otherwise
*/
public boolean isExplicitlyDisabled(IFolder folder) {
if (folder == null)
return false;
return DisabledResourceManager.getDefault().isDisabled(folder);
}
/**
* Returns the explicitly disabled parent library folder of the given
* resource.
*
* <p>
* This is a parent folder that was passed as parameter to the
* {@link #disableValidation(IFolder)} method.
* </p>
*
* @param resource
* a resource
*
* @return the explicitly disabled parent library folder of the given
* resource
*/
public IFolder getExplicitlyDisabledParent(IResource resource) {
if (resource == null)
return null;
if (resource.getType() == IResource.FILE) {
// the resource is a source file, so take its parent folder
resource = resource.getParent();
}
while (resource.getType() == IResource.FOLDER) {
IFolder folder = (IFolder) resource;
// check if the folder is disabled in the WTP Validation Framework
if (isExplicitlyDisabled(folder)) {
return folder;
}
// the folder is not disabled, so check its parent folder
resource = resource.getParent();
}
// none of the resource's parent folders is disabled in the WTP
// Validation Framework, so the resource is not inside a library folder
// and therefore has no explicitly disabled parent library folder
return null;
}
/**
* Returns all subfolders (recursively) of the given folders.
*
* <p>
* This method invokes {@link #getAllSubfolders(IFolder)} for each of the
* folders in the given array and merges the result.
* </p>
*
* @param folders
* an array of folders
*
* @return an array of folders containing the folders of the given array and
* all their subfolders
*
* @throws CoreException
* if any of the folders does not exist or is in a closed
* project
*/
public IFolder[] getAllSubfolders(IFolder[] folders) throws CoreException {
Collection<IFolder> allSubfolders = new HashSet<IFolder>();
for (IFolder folder : folders) {
allSubfolders.addAll(Arrays.asList(getAllSubfolders(folder)));
}
return allSubfolders.toArray(new IFolder[allSubfolders.size()]);
}
/**
* Returns all subfolders (recursively) of the given folder.
*
* <p>
* This method traverses the complete folder's subtree to find the
* subfolders on all levels.
* </p>
*
* @param folder
* a folder
*
* @return an array of folders containing the given folder and all its
* subfolders
*
* @throws CoreException
* if the folder does not exist or is in a closed project
*/
public IFolder[] getAllSubfolders(IFolder folder) throws CoreException {
List<IFolder> result = new ArrayList<IFolder>();
collectAllSubfolders((IFolder) folder, result);
return result.toArray(new IFolder[result.size()]);
}
/**
* WARNING: This method should not be used by the clients.
*
* Enables/disables detection for given project.
*
* @param project
* @param suspend
*/
public void suspendDetection(IProject project, boolean suspend) {
listener.suspendDetection(project, suspend);
}
/**
* WARNING: This method should not be used by the clients.
*
* Enables/disables whole detection.
*
* @param suspend
*/
public void suspendAllDetection(boolean suspend) {
listener.suspendAllDetection(suspend);
}
/**
* Add the given folder and all its subfolders (recursively) to the given
* result collection.
*
* <p>
* This method traverses the complete folder's subtree to find the
* subfolders on all levels.
* </p>
*
* @param folder
* a folder
* @param result
* a collection of folder to hold the result
*
* @throws CoreException
* if the folder does not exist or is in a closed project
*/
private void collectAllSubfolders(IFolder folder, Collection<IFolder> result) throws CoreException {
result.add(folder);
if (!folder.exists()) {
return;
}
for (IResource child : folder.members()) {
if (child.getType() == IResource.FOLDER) {
collectAllSubfolders((IFolder) child, result);
}
}
}
/**
* Notifies the registered library folder change listeners that the given
* folders have changed from source folders to library folders or vice
* versa.
*
* @param folders
* the changed folders
*/
private void notifyListeners(IFolder[] folders) {
synchronized (listeners) {
for (ILibraryFolderChangeListener listener : listeners) {
try {
listener.foldersChanged(folders);
} catch (Exception e) {
PHPCorePlugin.log(e);
}
}
}
}
/**
* Removes the non-existing folders from the given array.
*
* <p>
* The result is returned as new array. The given array remains untouched.
* </p>
*
* @param folders
* an array of folders
*
* @return a new array that contains only the existing folders of the given
* array
*/
private IFolder[] removeNonExisting(IFolder[] folders) {
Collection<IFolder> existing = new HashSet<IFolder>();
for (IFolder folder : folders) {
if (folder.exists()) {
existing.add(folder);
}
}
return existing.toArray(new IFolder[existing.size()]);
}
/**
* Delete all problem and tasks markers on all the given resources and all
* their children.
*
* <p>
* Only markers of types {@link IMarker#PROBLEM} and {@link IMarker#TASK}
* and their subtypes are deleted. Markers of other types (bookmarks,
* breakpoints, etc.) are not affected.
* </p>
*
* @param resources
* an array of resources
*
* @throws CoreException
* if deleting the markers on any resource fails
*/
private void deleteMarkers(IResource[] resources) throws CoreException {
for (IResource resource : resources) {
resource.deleteMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
resource.deleteMarkers(IMarker.TASK, true, IResource.DEPTH_INFINITE);
}
}
/**
* Triggers validation jobs for the given resources and all their children.
*
* @param resources
* an array of resources
* @param monitor
* a progress monitor, or null if progress reporting is not
* desired
*
* @throws CoreException
* if touching of any of the resources fails
*/
private void revalidate(IResource[] resources, IProgressMonitor monitor) throws CoreException {
for (IResource resource : resources) {
deepTouch(resource, monitor);
}
}
/**
* Invokes {@link IResource#touch(IProgressMonitor)} on the given resource
* and all its children.
*
* @param resource
* a resource to touch
* @param monitor
* a progress monitor, or null if progress reporting is not
* desired
*
* @throws CoreException
* if touching of any of the resources fails
*/
private void deepTouch(IResource resource, IProgressMonitor monitor) throws CoreException {
resource.touch(monitor);
// touch recursively resources inside folders and projects
if (resource instanceof IContainer) {
IContainer container = (IContainer) resource;
for (IResource member : container.members()) {
deepTouch(member, monitor);
}
}
}
/**
* Waits for any running jobs of the WTP Validation Framework to finish.
*
* @param monitor
* a progress monitor, or null if progress reporting is not
* desired
*
* @throws OperationCanceledException
* if the progress monitor is canceled while waiting
* @throws InterruptedException
* if this thread is interrupted while waiting
*/
private void waitValidationJobs(IProgressMonitor monitor) throws OperationCanceledException, InterruptedException {
Job.getJobManager().join(ResourcesPlugin.FAMILY_MANUAL_BUILD, monitor);
}
}