Copyright (c) 2000, 2015 IBM Corporation and others. This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which accompanies this distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ SPDX-License-Identifier: EPL-2.0 Contributors: IBM Corporation - initial API and implementation James Blackburn (Broadcom Corp.) - ongoing development Lars Vogel - Bug 473427
/******************************************************************************* * Copyright (c) 2000, 2015 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * Contributors: * IBM Corporation - initial API and implementation * James Blackburn (Broadcom Corp.) - ongoing development * Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427 *******************************************************************************/
package org.eclipse.core.internal.resources; import java.util.*; import org.eclipse.core.internal.events.ILifecycleListener; import org.eclipse.core.internal.events.LifecycleEvent; import org.eclipse.core.internal.utils.Messages; import org.eclipse.core.internal.utils.Policy; import org.eclipse.core.resources.*; import org.eclipse.core.runtime.*; import org.eclipse.osgi.util.NLS;
Maintains collection of known nature descriptors, and implements nature-related algorithms provided by the workspace.
/** * Maintains collection of known nature descriptors, and implements * nature-related algorithms provided by the workspace. */
public class NatureManager implements ILifecycleListener, IManager { //maps String (nature ID) -> descriptor objects private Map<String, IProjectNatureDescriptor> descriptors; //maps IProject -> String[] of enabled natures for that project private final Map<Project, String[]> natureEnablements = new HashMap<>(20); //maps String (builder ID) -> String (nature ID) private Map<String, String> buildersToNatures; //colour constants used in cycle detection algorithm private static final byte WHITE = 0; private static final byte GREY = 1; private static final byte BLACK = 2; protected NatureManager() { super(); }
Computes the list of natures that are enabled for the given project. Enablement computation is subtly different from nature set validation, because it must find and remove all inconsistencies.
/** * Computes the list of natures that are enabled for the given project. * Enablement computation is subtly different from nature set * validation, because it must find and remove all inconsistencies. */
protected String[] computeNatureEnablements(Project project) { final ProjectDescription description = project.internalGetDescription(); if (description == null) return new String[0];//project deleted concurrently String[] natureIds = description.getNatureIds(); int count = natureIds.length; if (count == 0) return natureIds; //set of the nature ids being validated (String (id)) HashSet<String> candidates = new HashSet<>(count * 2); //table of String(set ID) -> ArrayList (nature IDs that belong to that set) HashMap<String, ArrayList<String>> setsToNatures = new HashMap<>(count); for (int i = 0; i < count; i++) { String id = natureIds[i]; ProjectNatureDescriptor desc = (ProjectNatureDescriptor) getNatureDescriptor(id); if (desc == null) continue; if (!desc.hasCycle) candidates.add(id); //build set to nature map String[] setIds = desc.getNatureSetIds(); for (String set : setIds) { ArrayList<String> current = setsToNatures.get(set); if (current == null) { current = new ArrayList<>(5); setsToNatures.put(set, current); } current.add(id); } } //now remove all natures that belong to sets with more than one member for (ArrayList<String> setMembers : setsToNatures.values()) { if (setMembers.size() > 1) { candidates.removeAll(setMembers); } } //now walk over the set and ensure all pre-requisite natures are present //need to walk in prereq order because if A requires B and B requires C, and C is //disabled for some other reason, we must ensure both A and B are disabled String[] orderedCandidates = candidates.toArray(new String[candidates.size()]); orderedCandidates = sortNatureSet(orderedCandidates); for (String id : orderedCandidates) { IProjectNatureDescriptor desc = getNatureDescriptor(id); String[] required = desc.getRequiredNatureIds(); for (String t : required) { if (!candidates.contains(t)) { candidates.remove(id); break; } } } //remaining candidates are enabled return candidates.toArray(new String[candidates.size()]); } /* (non-Javadoc) * @see IWorkspace#getNatureDescriptor(String) */ public synchronized IProjectNatureDescriptor getNatureDescriptor(String natureId) { lazyInitialize(); return descriptors.get(natureId); } /* (non-Javadoc) * @see IWorkspace#getNatureDescriptors() */ public synchronized IProjectNatureDescriptor[] getNatureDescriptors() { lazyInitialize(); Collection<IProjectNatureDescriptor> values = descriptors.values(); return values.toArray(new IProjectNatureDescriptor[values.size()]); } @Override public void handleEvent(LifecycleEvent event) { switch (event.kind) { case LifecycleEvent.POST_PROJECT_CHANGE : case LifecycleEvent.PRE_PROJECT_CLOSE : case LifecycleEvent.PRE_PROJECT_DELETE : case LifecycleEvent.PRE_PROJECT_MOVE : case LifecycleEvent.PRE_PROJECT_OPEN : flushEnablements((IProject) event.resource); } }
Configures the nature with the given ID for the given project.
/** * Configures the nature with the given ID for the given project. */
protected void configureNature(final Project project, final String natureID, final MultiStatus errors) { ISafeRunnable code = new ISafeRunnable() { @Override public void run() throws Exception { IProjectNature nature = createNature(project, natureID); nature.configure(); ProjectInfo info = (ProjectInfo) project.getResourceInfo(false, true); info.setNature(natureID, nature); } @Override public void handleException(Throwable exception) { if (exception instanceof CoreException) errors.add(((CoreException) exception).getStatus()); else errors.add(new ResourceStatus(IResourceStatus.INTERNAL_ERROR, project.getFullPath(), NLS.bind(Messages.resources_errorNature, natureID), exception)); } }; if (Policy.DEBUG_NATURES) { Policy.debug("Configuring nature: " + natureID + " on project: " + project.getName()); //$NON-NLS-1$ //$NON-NLS-2$ } SafeRunner.run(code); }
Configures the natures for the given project. Natures found in the new description that weren't present in the old description are added, and natures missing from the new description are removed. Updates the old description so that it reflects the new set of the natures. Errors are added to the given multi-status.
/** * Configures the natures for the given project. Natures found in the new description * that weren't present in the old description are added, and natures missing from the * new description are removed. Updates the old description so that it reflects * the new set of the natures. Errors are added to the given multi-status. */
@SuppressWarnings({"unchecked"}) public void configureNatures(Project project, ProjectDescription oldDescription, ProjectDescription newDescription, MultiStatus status) { // Be careful not to rely on much state because (de)configuring a nature // may well result in recursive calls to this method. HashSet<String> oldNatures = new HashSet<>(Arrays.asList(oldDescription.getNatureIds(false))); HashSet<String> newNatures = new HashSet<>(Arrays.asList(newDescription.getNatureIds(false))); if (oldNatures.equals(newNatures)) return; HashSet<String> deletions = (HashSet<String>) oldNatures.clone(); HashSet<String> additions = (HashSet<String>) newNatures.clone(); additions.removeAll(oldNatures); deletions.removeAll(newNatures); //do validation of the changes. If any single change is invalid, fail the whole operation IStatus result = validateAdditions(newNatures, additions, project); if (!result.isOK()) { status.merge(result); return; } result = validateRemovals(newNatures, deletions); if (!result.isOK()) { status.merge(result); return; } // set the list of nature ids BEFORE (de)configuration so recursive calls will // not try to do the same work. oldDescription.setNatureIds(newDescription.getNatureIds(true)); flushEnablements(project); //(de)configure in topological order to maintain consistency of configured set String[] ordered = null; if (deletions.size() > 0) { ordered = sortNatureSet(deletions.toArray(new String[deletions.size()])); for (int i = ordered.length; --i >= 0;) deconfigureNature(project, ordered[i], status); } if (additions.size() > 0) { ordered = sortNatureSet(additions.toArray(new String[additions.size()])); for (String element : ordered) configureNature(project, element, status); } }
Finds the nature extension, and initializes and returns an instance.
/** * Finds the nature extension, and initializes and returns an instance. */
protected IProjectNature createNature(Project project, String natureID) throws CoreException { IExtension extension = Platform.getExtensionRegistry().getExtension(ResourcesPlugin.PI_RESOURCES, ResourcesPlugin.PT_NATURES, natureID); if (extension == null) { String message = NLS.bind(Messages.resources_natureExtension, natureID); throw new ResourceException(Platform.PLUGIN_ERROR, project.getFullPath(), message, null); } IConfigurationElement[] configs = extension.getConfigurationElements(); if (configs.length < 1) { String message = NLS.bind(Messages.resources_natureClass, natureID); throw new ResourceException(Platform.PLUGIN_ERROR, project.getFullPath(), message, null); } //find the runtime configuration element IConfigurationElement config = null; for (int i = 0; config == null && i < configs.length; i++) if ("runtime".equalsIgnoreCase(configs[i].getName())) //$NON-NLS-1$ config = configs[i]; if (config == null) { String message = NLS.bind(Messages.resources_natureFormat, natureID); throw new ResourceException(Platform.PLUGIN_ERROR, project.getFullPath(), message, null); } try { IProjectNature nature = (IProjectNature) config.createExecutableExtension("run"); //$NON-NLS-1$ nature.setProject(project); return nature; } catch (ClassCastException e) { String message = NLS.bind(Messages.resources_natureImplement, natureID); throw new ResourceException(Platform.PLUGIN_ERROR, project.getFullPath(), message, e); } }
Deconfigures the nature with the given ID for the given project.
/** * Deconfigures the nature with the given ID for the given project. */
protected void deconfigureNature(final Project project, final String natureID, final MultiStatus status) { final ProjectInfo info = (ProjectInfo) project.getResourceInfo(false, true); IProjectNature existingNature = info.getNature(natureID); if (existingNature == null) { // if there isn't a nature then create one so we can deconfig it. try { existingNature = createNature(project, natureID); } catch (CoreException e) { // Ignore - we are removing a nature that no longer exists in the install return; } } final IProjectNature nature = existingNature; ISafeRunnable code = new ISafeRunnable() { @Override public void run() throws Exception { nature.deconfigure(); info.setNature(natureID, null); } @Override public void handleException(Throwable exception) { if (exception instanceof CoreException) status.add(((CoreException) exception).getStatus()); else status.add(new ResourceStatus(IResourceStatus.INTERNAL_ERROR, project.getFullPath(), NLS.bind(Messages.resources_natureDeconfig, natureID), exception)); } }; if (Policy.DEBUG_NATURES) { Policy.debug("Deconfiguring nature: " + natureID + " on project: " + project.getName()); //$NON-NLS-1$ //$NON-NLS-2$ } SafeRunner.run(code); }
Marks all nature descriptors that are involved in cycles
/** * Marks all nature descriptors that are involved in cycles */
private void detectCycles() { Collection<IProjectNatureDescriptor> values = descriptors.values(); ProjectNatureDescriptor[] natures = values.toArray(new ProjectNatureDescriptor[values.size()]); for (ProjectNatureDescriptor nature : natures) if (nature.colour == WHITE) hasCycles(nature); }
Returns a status indicating failure to configure natures.
/** * Returns a status indicating failure to configure natures. */
protected IStatus failure(String reason) { return new ResourceStatus(IResourceStatus.INVALID_NATURE_SET, reason); }
Returns the ID of the project nature that claims ownership of the builder with the given ID. Returns null if no nature owns that builder.
/** * Returns the ID of the project nature that claims ownership of the * builder with the given ID. Returns null if no nature owns that builder. */
public synchronized String findNatureForBuilder(String builderID) { if (buildersToNatures == null) { buildersToNatures = new HashMap<>(10); IProjectNatureDescriptor[] descs = getNatureDescriptors(); for (IProjectNatureDescriptor desc : descs) { String natureId = desc.getNatureId(); String[] builders = ((ProjectNatureDescriptor) desc).getBuilderIds(); for (String builder : builders) { //FIXME: how to handle multiple natures specifying same builder buildersToNatures.put(builder, natureId); } } } return buildersToNatures.get(builderID); } private synchronized void flushEnablements(IProject project) { natureEnablements.remove(project); }
Returns the cached array of enabled natures for this project, or null if there is nothing in the cache.
/** * Returns the cached array of enabled natures for this project, * or null if there is nothing in the cache. */
protected synchronized String[] getEnabledNatures(Project project) { String[] enabled = natureEnablements.get(project); if (enabled != null) return enabled; enabled = computeNatureEnablements(project); natureEnablements.put(project, enabled); return enabled; }
Returns true if there are cycles in the graph of nature dependencies starting at root i. Returns false otherwise. Marks all descriptors that are involved in the cycle as invalid.
/** * Returns true if there are cycles in the graph of nature * dependencies starting at root i. Returns false otherwise. * Marks all descriptors that are involved in the cycle as invalid. */
protected boolean hasCycles(ProjectNatureDescriptor desc) { if (desc.colour == BLACK) { //this subgraph has already been traversed, so we know the answer return desc.hasCycle; } //if we are already grey, then we have found a cycle if (desc.colour == GREY) { desc.hasCycle = true; desc.colour = BLACK; return true; } //colour current descriptor GREY to indicate it is being visited desc.colour = GREY; //visit all dependents of nature i String[] required = desc.getRequiredNatureIds(); for (String element : required) { ProjectNatureDescriptor dependency = (ProjectNatureDescriptor) getNatureDescriptor(element); //missing dependencies cannot create cycles if (dependency != null && hasCycles(dependency)) { desc.hasCycle = true; desc.colour = BLACK; return true; } } desc.hasCycle = false; desc.colour = BLACK; return false; }
Returns true if the given project has linked resources, and false otherwise.
/** * Returns true if the given project has linked resources, and false otherwise. */
protected boolean hasLinks(IProject project) { try { IResource[] children = project.members(); for (IResource element : children) if (element.isLinked()) return true; } catch (CoreException e) { //not possible for project to be inaccessible Policy.log(e.getStatus()); } return false; }
Checks if the two natures have overlapping "one-of-nature" set memberships. Returns the name of one such overlap, or null if there is no set overlap.
/** * Checks if the two natures have overlapping "one-of-nature" set * memberships. Returns the name of one such overlap, or null if * there is no set overlap. */
protected String hasSetOverlap(IProjectNatureDescriptor one, IProjectNatureDescriptor two) { if (one == null || two == null) { return null; } //efficiency not so important because these sets are very small String[] setsOne = one.getNatureSetIds(); String[] setsTwo = two.getNatureSetIds(); for (String element : setsOne) { for (String element2 : setsTwo) { if (element.equals(element2)) { return element; } } } return null; }
Perform depth-first insertion of the given nature ID into the result list.
/** * Perform depth-first insertion of the given nature ID into the result list. */
protected void insert(ArrayList<String> list, Set<String> seen, String id) { if (seen.contains(id)) return; seen.add(id); //insert prerequisite natures IProjectNatureDescriptor desc = getNatureDescriptor(id); if (desc != null) { String[] prereqs = desc.getRequiredNatureIds(); for (String prereq : prereqs) insert(list, seen, prereq); } list.add(id); } /* (non-Javadoc) * Returns true if the given nature is enabled for the given project. * * @see IProject#isNatureEnabled(String) */ public boolean isNatureEnabled(Project project, String id) { String[] enabled = getEnabledNatures(project); for (String element : enabled) { if (element.equals(id)) return true; } return false; }
Only initialize the descriptor cache when we know it is actually needed. Running programs may never need to refer to this cache.
/** * Only initialize the descriptor cache when we know it is actually needed. * Running programs may never need to refer to this cache. */
private void lazyInitialize() { if (descriptors != null) return; IExtensionPoint point = Platform.getExtensionRegistry().getExtensionPoint(ResourcesPlugin.PI_RESOURCES, ResourcesPlugin.PT_NATURES); IExtension[] extensions = point.getExtensions(); descriptors = new HashMap<>(extensions.length * 2 + 1); for (IExtension extension : extensions) { IProjectNatureDescriptor desc = null; try { desc = new ProjectNatureDescriptor(extension); } catch (CoreException e) { Policy.log(e.getStatus()); } if (desc != null) descriptors.put(desc.getNatureId(), desc); } //do cycle detection now so it only has to be done once //cycle detection on a graph subset is a pain detectCycles(); } @Override public void shutdown(IProgressMonitor monitor) { // do nothing } /* (non-Javadoc) * @see IWorkspace#sortNatureSet(String[]) */ public String[] sortNatureSet(String[] natureIds) { int count = natureIds.length; if (count == 0) return natureIds; ArrayList<String> result = new ArrayList<>(count); HashSet<String> seen = new HashSet<>(count);//for cycle and duplicate detection for (int i = 0; i < count; i++) insert(result, seen, natureIds[i]); //remove added prerequisites that didn't exist in original list seen.clear(); seen.addAll(Arrays.asList(natureIds)); for (Iterator<String> it = result.iterator(); it.hasNext();) { Object id = it.next(); if (!seen.contains(id)) it.remove(); } return result.toArray(new String[result.size()]); } @Override public void startup(IProgressMonitor monitor) { ((Workspace) ResourcesPlugin.getWorkspace()).addLifecycleListener(this); }
Validates the given nature additions in the nature set for this project. Tolerates existing inconsistencies in the nature set.
Params:
  • newNatures – the complete new set of nature IDs for the project, including additions
  • additions – the subset of newNatures that represents natures being added
Returns:An OK status if all additions are valid, and an error status if any of the additions introduce new inconsistencies.
/** * Validates the given nature additions in the nature set for this * project. Tolerates existing inconsistencies in the nature set. * @param newNatures the complete new set of nature IDs for the project, * including additions * @param additions the subset of newNatures that represents natures * being added * @return An OK status if all additions are valid, and an error status * if any of the additions introduce new inconsistencies. */
protected IStatus validateAdditions(HashSet<String> newNatures, HashSet<String> additions, IProject project) { Boolean hasLinks = null;//three states: true, false, null (not yet computed) //perform checks in order from least expensive to most expensive for (String id : additions) { // check for adding a nature that is not available. IProjectNatureDescriptor desc = getNatureDescriptor(id); if (desc == null) { return failure(NLS.bind(Messages.natures_missingNature, id)); } // check for adding a nature that creates a circular dependency if (((ProjectNatureDescriptor) desc).hasCycle) { return failure(NLS.bind(Messages.natures_hasCycle, id)); } // check for adding a nature that has a missing prerequisite. String[] required = desc.getRequiredNatureIds(); for (String r : required) { if (!newNatures.contains(r)) { return failure(NLS.bind(Messages.natures_missingPrerequisite, id, r)); } } // check for adding a nature that creates a duplicated set member. for (String current : newNatures) { if (!current.equals(id)) { String overlap = hasSetOverlap(desc, getNatureDescriptor(current)); if (overlap != null) { return failure(NLS.bind(Messages.natures_multipleSetMembers, overlap)); } } } //check for adding a nature that has linked resource veto if (!desc.isLinkingAllowed()) { if (hasLinks == null) { hasLinks = hasLinks(project) ? Boolean.TRUE : Boolean.FALSE; } if (hasLinks.booleanValue()) return failure(NLS.bind(Messages.links_vetoNature, project.getName(), id)); } } return Status.OK_STATUS; }
Validates whether a project with the given set of natures should allow linked resources. Returns an OK status if linking is allowed, otherwise a non-OK status indicating why linking is not allowed. Linking is allowed if there is no project nature that explicitly disallows it. No validation is done on the nature ids themselves (ids that don't have a corresponding nature definition will be ignored).
/** * Validates whether a project with the given set of natures should allow * linked resources. Returns an OK status if linking is allowed, * otherwise a non-OK status indicating why linking is not allowed. * Linking is allowed if there is no project nature that explicitly disallows it. * No validation is done on the nature ids themselves (ids that don't have * a corresponding nature definition will be ignored). */
public IStatus validateLinkCreation(String[] natureIds) { for (String natureId : natureIds) { IProjectNatureDescriptor desc = getNatureDescriptor(natureId); if (desc != null && !desc.isLinkingAllowed()) { String msg = NLS.bind(Messages.links_natureVeto, desc.getLabel()); return new ResourceStatus(IResourceStatus.LINKING_NOT_ALLOWED, msg); } } return Status.OK_STATUS; }
Validates the given nature removals in the nature set for this project. Tolerates existing inconsistencies in the nature set.
Params:
  • newNatures – the complete new set of nature IDs for the project, excluding deletions
  • deletions – the nature IDs that are being removed from the set.
Returns:An OK status if all removals are valid, and a not OK status if any of the deletions introduce new inconsistencies.
/** * Validates the given nature removals in the nature set for this * project. Tolerates existing inconsistencies in the nature set. * * @param newNatures the complete new set of nature IDs for the project, * excluding deletions * @param deletions the nature IDs that are being removed from the set. * @return An OK status if all removals are valid, and a not OK status * if any of the deletions introduce new inconsistencies. */
protected IStatus validateRemovals(HashSet<String> newNatures, HashSet<String> deletions) { //iterate over new nature set, and ensure that none of their prerequisites are being deleted for (String currentID : newNatures) { IProjectNatureDescriptor desc = getNatureDescriptor(currentID); if (desc != null) { String[] required = desc.getRequiredNatureIds(); for (String element : required) { if (deletions.contains(element)) { return failure(NLS.bind(Messages.natures_invalidRemoval, element, currentID)); } } } } return Status.OK_STATUS; } /* (non-Javadoc) * @see IWorkspace#validateNatureSet(String[]) */ public IStatus validateNatureSet(String[] natureIds) { int count = natureIds.length; if (count == 0) return Status.OK_STATUS; String msg = Messages.natures_invalidSet; MultiStatus result = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INVALID_NATURE_SET, msg, null); //set of the nature ids being validated (String (id)) HashSet<String> natures = new HashSet<>(count * 2); //set of nature sets for which a member nature has been found (String (id)) HashSet<String> sets = new HashSet<>(count); for (int i = 0; i < count; i++) { String id = natureIds[i]; ProjectNatureDescriptor desc = (ProjectNatureDescriptor) getNatureDescriptor(id); if (desc == null) { result.add(failure(NLS.bind(Messages.natures_missingNature, id))); continue; } if (desc.hasCycle) result.add(failure(NLS.bind(Messages.natures_hasCycle, id))); if (!natures.add(id)) result.add(failure(NLS.bind(Messages.natures_duplicateNature, id))); //validate nature set one-of constraint String[] setIds = desc.getNatureSetIds(); for (String setId : setIds) { if (!sets.add(setId)) { result.add(failure(NLS.bind(Messages.natures_multipleSetMembers, setId))); } } } //now walk over the set and ensure all pre-requisite natures are present for (int i = 0; i < count; i++) { IProjectNatureDescriptor desc = getNatureDescriptor(natureIds[i]); if (desc == null) continue; String[] required = desc.getRequiredNatureIds(); for (String r : required) { if (!natures.contains(r)) { result.add(failure(NLS.bind(Messages.natures_missingPrerequisite, natureIds[i], r))); } } } //if there are no problems we must return a status whose code is OK return result.isOK() ? Status.OK_STATUS : (IStatus) result; } }