/******************************************************************************* * Copyright (c) 2000, 2011 IBM Corporation 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: * IBM Corporation - initial API and implementation * James Blackburn (Broadcom Corp.) - ongoing development *******************************************************************************/ 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. */ 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<Project, String[]>(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. */ 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<String>(count * 2); //table of String(set ID) -> ArrayList (nature IDs that belong to that set) HashMap<String, ArrayList<String>> setsToNatures = new HashMap<String, ArrayList<String>>(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 (int j = 0; j < setIds.length; j++) { String set = setIds[j]; ArrayList<String> current = setsToNatures.get(set); if (current == null) { current = new ArrayList<String>(5); setsToNatures.put(set, current); } current.add(id); } } //now remove all natures that belong to sets with more than one member for (Iterator<ArrayList<String>> it = setsToNatures.values().iterator(); it.hasNext();) { ArrayList<String> setMembers = it.next(); 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 (int i = 0; i < orderedCandidates.length; i++) { String id = orderedCandidates[i]; IProjectNatureDescriptor desc = getNatureDescriptor(id); String[] required = desc.getRequiredNatureIds(); for (int j = 0; j < required.length; j++) { if (!candidates.contains(required[j])) { 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()]); } 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. */ protected void configureNature(final Project project, final String natureID, final MultiStatus errors) { ISafeRunnable code = new ISafeRunnable() { public void run() throws Exception { IProjectNature nature = createNature(project, natureID); nature.configure(); ProjectInfo info = (ProjectInfo) project.getResourceInfo(false, true); info.setNature(natureID, nature); } 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) { System.out.println("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. */ @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<String>(Arrays.asList(oldDescription.getNatureIds(false))); HashSet<String> newNatures = new HashSet<String>(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 (int i = 0; i < ordered.length; i++) configureNature(project, ordered[i], status); } } /** * 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. */ 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() { public void run() throws Exception { nature.deconfigure(); info.setNature(natureID, null); } 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) { System.out.println("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 */ private void detectCycles() { Collection<IProjectNatureDescriptor> values = descriptors.values(); ProjectNatureDescriptor[] natures = values.toArray(new ProjectNatureDescriptor[values.size()]); for (int i = 0; i < natures.length; i++) if (natures[i].colour == WHITE) hasCycles(natures[i]); } /** * 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. */ public synchronized String findNatureForBuilder(String builderID) { if (buildersToNatures == null) { buildersToNatures = new HashMap<String, String>(10); IProjectNatureDescriptor[] descs = getNatureDescriptors(); for (int i = 0; i < descs.length; i++) { String natureId = descs[i].getNatureId(); String[] builders = ((ProjectNatureDescriptor) descs[i]).getBuilderIds(); for (int j = 0; j < builders.length; j++) { //FIXME: how to handle multiple natures specifying same builder buildersToNatures.put(builders[j], 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. */ 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. */ 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 (int i = 0; i < required.length; i++) { ProjectNatureDescriptor dependency = (ProjectNatureDescriptor) getNatureDescriptor(required[i]); //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. */ protected boolean hasLinks(IProject project) { try { IResource[] children = project.members(); for (int i = 0; i < children.length; i++) if (children[i].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. */ 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 (int iOne = 0; iOne < setsOne.length; iOne++) { for (int iTwo = 0; iTwo < setsTwo.length; iTwo++) { if (setsOne[iOne].equals(setsTwo[iTwo])) { return setsOne[iOne]; } } } return null; } /** * 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 (int i = 0; i < prereqs.length; i++) insert(list, seen, prereqs[i]); } 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 (int i = 0; i < enabled.length; i++) { if (enabled[i].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. */ 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<String, IProjectNatureDescriptor>(extensions.length * 2 + 1); for (int i = 0, imax = extensions.length; i < imax; i++) { IProjectNatureDescriptor desc = null; try { desc = new ProjectNatureDescriptor(extensions[i]); } 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(); } 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<String>(count); HashSet<String> seen = new HashSet<String>(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()]); } 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. * @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 (Iterator<String> added = additions.iterator(); added.hasNext();) { String id = added.next(); // 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 (int i = 0; i < required.length; i++) { if (!newNatures.contains(required[i])) { return failure(NLS.bind(Messages.natures_missingPrerequisite, id, required[i])); } } // check for adding a nature that creates a duplicated set member. for (Iterator<String> all = newNatures.iterator(); all.hasNext();) { String current = all.next(); 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). */ public IStatus validateLinkCreation(String[] natureIds) { for (int i = 0; i < natureIds.length; i++) { IProjectNatureDescriptor desc = getNatureDescriptor(natureIds[i]); 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. * * @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 (Iterator<String> it = newNatures.iterator(); it.hasNext();) { String currentID = it.next(); IProjectNatureDescriptor desc = getNatureDescriptor(currentID); if (desc != null) { String[] required = desc.getRequiredNatureIds(); for (int i = 0; i < required.length; i++) { if (deletions.contains(required[i])) { return failure(NLS.bind(Messages.natures_invalidRemoval, required[i], 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<String>(count * 2); //set of nature sets for which a member nature has been found (String (id)) HashSet<String> sets = new HashSet<String>(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 (int j = 0; j < setIds.length; j++) { if (!sets.add(setIds[j])) result.add(failure(NLS.bind(Messages.natures_multipleSetMembers, setIds[j]))); } } //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 (int j = 0; j < required.length; j++) if (!natures.contains(required[j])) result.add(failure(NLS.bind(Messages.natures_missingPrerequisite, natureIds[i], required[j]))); } //if there are no problems we must return a status whose code is OK return result.isOK() ? Status.OK_STATUS : (IStatus) result; } }