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 }