1 /******************************************************************************* 2 * Copyright (c) 2005, 2015 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 * James Blackburn (Broadcom Corp.) - ongoing development 14 * Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427 15 *******************************************************************************/ 16 package org.eclipse.core.internal.resources; 17 18 import java.util.*; 19 import org.eclipse.core.internal.utils.Cache; 20 import org.eclipse.core.resources.IProject; 21 import org.eclipse.core.resources.ProjectScope; 22 import org.eclipse.core.runtime.*; 23 import org.eclipse.core.runtime.content.IContentType; 24 import org.eclipse.core.runtime.content.IContentTypeManager.ISelectionPolicy; 25 import org.eclipse.core.runtime.content.IContentTypeMatcher; 26 import org.eclipse.core.runtime.preferences.*; 27 import org.osgi.service.prefs.BackingStoreException; 28 import org.osgi.service.prefs.Preferences; 29 30 /** 31 * Manages project-specific content type behavior. 32 * 33 * @see ContentDescriptionManager 34 * @see org.eclipse.core.runtime.content.IContentTypeManager.ISelectionPolicy 35 * @since 3.1 36 */ 37 public class ProjectContentTypes { 38 39 /** 40 * A project-aware content type selection policy. 41 * This class is also a dynamic scope context that will delegate to either 42 * project or instance scope depending on whether project specific settings were enabled 43 * for the project in question. 44 */ 45 private class ProjectContentTypeSelectionPolicy implements ISelectionPolicy, IScopeContext { 46 // corresponding project 47 private Project project; 48 // cached project scope 49 private IScopeContext projectScope; 50 ProjectContentTypeSelectionPolicy(Project project)51 public ProjectContentTypeSelectionPolicy(Project project) { 52 this.project = project; 53 this.projectScope = new ProjectScope(project); 54 } 55 56 @Override equals(Object obj)57 public boolean equals(Object obj) { 58 if (this == obj) 59 return true; 60 if (!(obj instanceof IScopeContext)) 61 return false; 62 IScopeContext other = (IScopeContext) obj; 63 if (!getName().equals(other.getName())) 64 return false; 65 IPath location = getLocation(); 66 return location == null ? other.getLocation() == null : location.equals(other.getLocation()); 67 } 68 getDelegate()69 private IScopeContext getDelegate() { 70 if (!usesContentTypePreferences(project.getName())) 71 return InstanceScope.INSTANCE; 72 return projectScope; 73 } 74 75 @Override getLocation()76 public IPath getLocation() { 77 return getDelegate().getLocation(); 78 } 79 80 @Override getName()81 public String getName() { 82 return getDelegate().getName(); 83 } 84 85 @Override getNode(String qualifier)86 public IEclipsePreferences getNode(String qualifier) { 87 return getDelegate().getNode(qualifier); 88 } 89 90 @Override hashCode()91 public int hashCode() { 92 return getName().hashCode(); 93 } 94 95 @Override select(IContentType[] candidates, boolean fileName, boolean content)96 public IContentType[] select(IContentType[] candidates, boolean fileName, boolean content) { 97 return ProjectContentTypes.this.select(project, candidates, fileName, content); 98 } 99 } 100 101 private static final String CONTENT_TYPE_PREF_NODE = "content-types"; //$NON-NLS-1$ 102 103 private static final String PREF_LOCAL_CONTENT_TYPE_SETTINGS = "enabled"; //$NON-NLS-1$ 104 private static final Preferences PROJECT_SCOPE = Platform.getPreferencesService().getRootNode().node(ProjectScope.SCOPE); 105 private Cache contentTypesPerProject; 106 private Workspace workspace; 107 usesContentTypePreferences(String projectName)108 static boolean usesContentTypePreferences(String projectName) { 109 try { 110 // be careful looking up for our node so not to create any nodes as side effect 111 Preferences node = PROJECT_SCOPE; 112 //TODO once bug 90500 is fixed, should be simpler 113 // for now, take the long way 114 if (!node.nodeExists(projectName)) 115 return false; 116 node = node.node(projectName); 117 if (!node.nodeExists(Platform.PI_RUNTIME)) 118 return false; 119 node = node.node(Platform.PI_RUNTIME); 120 if (!node.nodeExists(CONTENT_TYPE_PREF_NODE)) 121 return false; 122 node = node.node(CONTENT_TYPE_PREF_NODE); 123 return node.getBoolean(PREF_LOCAL_CONTENT_TYPE_SETTINGS, false); 124 } catch (BackingStoreException | IllegalStateException | IllegalArgumentException e) { 125 // exception treated when retrieving the project preferences 126 } 127 return false; 128 } 129 ProjectContentTypes(Workspace workspace)130 public ProjectContentTypes(Workspace workspace) { 131 this.workspace = workspace; 132 // keep cache small 133 this.contentTypesPerProject = new Cache(5, 30, 0.4); 134 } 135 136 /** 137 * Collect content types associated to the natures configured for the given project. 138 */ collectAssociatedContentTypes(Project project)139 private Set<String> collectAssociatedContentTypes(Project project) { 140 String[] enabledNatures = workspace.getNatureManager().getEnabledNatures(project); 141 if (enabledNatures.length == 0) 142 return Collections.EMPTY_SET; 143 Set<String> related = new HashSet<>(enabledNatures.length); 144 for (String enabledNature : enabledNatures) { 145 ProjectNatureDescriptor descriptor = (ProjectNatureDescriptor) workspace.getNatureDescriptor(enabledNature); 146 if (descriptor == null) 147 // no descriptor found for the nature, skip it 148 continue; 149 String[] natureContentTypes = descriptor.getContentTypeIds(); 150 related.addAll(Arrays.asList(natureContentTypes)); // collect associate content types 151 } 152 return related; 153 } 154 contentTypePreferencesChanged(IProject project)155 public void contentTypePreferencesChanged(IProject project) { 156 final ProjectInfo info = (ProjectInfo) ((Project) project).getResourceInfo(false, false); 157 if (info != null) 158 info.setMatcher(null); 159 } 160 161 /** 162 * Creates a content type matcher for the given project. Takes natures and user settings into account. 163 */ createMatcher(Project project)164 private IContentTypeMatcher createMatcher(Project project) { 165 ProjectContentTypeSelectionPolicy projectContentTypeSelectionPolicy = new ProjectContentTypeSelectionPolicy(project); 166 return Platform.getContentTypeManager().getMatcher(projectContentTypeSelectionPolicy, projectContentTypeSelectionPolicy); 167 } 168 169 @SuppressWarnings({"unchecked"}) getAssociatedContentTypes(Project project)170 private Set<String> getAssociatedContentTypes(Project project) { 171 final ResourceInfo info = project.getResourceInfo(false, false); 172 if (info == null) 173 // the project has been deleted 174 return null; 175 final String projectName = project.getName(); 176 synchronized (contentTypesPerProject) { 177 Cache.Entry entry = contentTypesPerProject.getEntry(projectName); 178 if (entry != null) 179 // we have an entry... 180 if (entry.getTimestamp() == info.getContentId()) 181 // ...and it is not stale, so just return it 182 return (Set<String>) entry.getCached(); 183 // no cached information found, have to collect associated content types 184 Set<String> result = collectAssociatedContentTypes(project); 185 if (entry == null) 186 // there was no entry before - create one 187 entry = contentTypesPerProject.addEntry(projectName, result, info.getContentId()); 188 else { 189 // just update the existing entry 190 entry.setTimestamp(info.getContentId()); 191 entry.setCached(result); 192 } 193 return result; 194 } 195 } 196 getMatcherFor(Project project)197 public IContentTypeMatcher getMatcherFor(Project project) throws CoreException { 198 ProjectInfo info = (ProjectInfo) project.getResourceInfo(false, false); 199 //fail if project has been deleted concurrently 200 if (info == null) 201 project.checkAccessible(project.getFlags(null)); 202 IContentTypeMatcher matcher = info.getMatcher(); 203 if (matcher != null) 204 return matcher; 205 matcher = createMatcher(project); 206 info.setMatcher(matcher); 207 return matcher; 208 } 209 210 /** 211 * Implements project specific, nature-based selection policy. No content types are vetoed. 212 * 213 * The criteria for this policy is as follows: 214 * <ol> 215 * <li>associated content types should appear before non-associated content types</li> 216 * <li>otherwise, relative ordering should be preserved.</li> 217 * </ol> 218 * 219 * @see org.eclipse.core.runtime.content.IContentTypeManager.ISelectionPolicy 220 */ select(Project project, IContentType[] candidates, boolean fileName, boolean content)221 final IContentType[] select(Project project, IContentType[] candidates, boolean fileName, boolean content) { 222 // since no vetoing is done here, don't go further if there is nothing to sort 223 if (candidates.length < 2) 224 return candidates; 225 final Set<String> associated = getAssociatedContentTypes(project); 226 if (associated == null || associated.isEmpty()) 227 // project has no content types associated 228 return candidates; 229 int associatedCount = 0; 230 for (int i = 0; i < candidates.length; i++) 231 // is it an associated content type? 232 if (associated.contains(candidates[i].getId())) { 233 // need to move it to the right spot (unless all types visited so far are associated as well) 234 if (associatedCount < i) { 235 final IContentType promoted = candidates[i]; 236 // move all non-associated content types before it one one position up... 237 for (int j = i; j > associatedCount; j--) 238 candidates[j] = candidates[j - 1]; 239 // ...so there is an empty spot for the content type we are promoting 240 candidates[associatedCount] = promoted; 241 } 242 associatedCount++; 243 } 244 return candidates; 245 } 246 }