Skip to content

Commit a12b7d8

Browse files
Copilotlaeubi
andcommitted
Add hierarchical ProjectPreferences support with tests
Co-authored-by: laeubi <[email protected]>
1 parent 02c4a8e commit a12b7d8

File tree

4 files changed

+631
-1
lines changed

4 files changed

+631
-1
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Vector Informatik GmbH 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+
* Vector Informatik GmbH - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.core.internal.resources;
15+
16+
import java.util.ArrayList;
17+
import java.util.Collections;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
import org.eclipse.core.resources.IProject;
22+
import org.eclipse.core.runtime.IPath;
23+
24+
/**
25+
* Cache for project nesting relationships. A project A is nested within a project B
26+
* if B.getLocation().isPrefixOf(A.getLocation()).
27+
*
28+
* This cache is invalidated when projects are created, deleted, moved, or opened/closed.
29+
*
30+
* @since 3.20
31+
*/
32+
public class ProjectNestingCache {
33+
/**
34+
* Cache mapping from a project to its list of ancestor projects (ordered from immediate parent to root).
35+
* The cache is cleared when projects change.
36+
*/
37+
private final Map<IProject, List<IProject>> nestingCache = new ConcurrentHashMap<>();
38+
39+
/**
40+
* Returns the list of ancestor projects for the given project, ordered from immediate parent to root.
41+
* A project B is an ancestor of project A if B.getLocation().isPrefixOf(A.getLocation()).
42+
*
43+
* @param project the project to find ancestors for
44+
* @param workspace the workspace containing all projects
45+
* @return list of ancestor projects, or empty list if no ancestors exist
46+
*/
47+
public List<IProject> getAncestorProjects(IProject project, Workspace workspace) {
48+
if (project == null || !project.isAccessible()) {
49+
return Collections.emptyList();
50+
}
51+
52+
// Check cache first
53+
List<IProject> cached = nestingCache.get(project);
54+
if (cached != null) {
55+
return cached;
56+
}
57+
58+
// Compute ancestor projects
59+
List<IProject> ancestors = computeAncestorProjects(project, workspace);
60+
nestingCache.put(project, ancestors);
61+
return ancestors;
62+
}
63+
64+
/**
65+
* Computes the ancestor projects for the given project.
66+
*/
67+
private List<IProject> computeAncestorProjects(IProject project, Workspace workspace) {
68+
IPath projectLocation = project.getLocation();
69+
if (projectLocation == null) {
70+
return Collections.emptyList();
71+
}
72+
73+
List<IProject> ancestors = new ArrayList<>();
74+
IProject[] allProjects = workspace.getRoot().getProjects();
75+
76+
for (IProject potentialAncestor : allProjects) {
77+
if (potentialAncestor.equals(project) || !potentialAncestor.isAccessible()) {
78+
continue;
79+
}
80+
81+
IPath ancestorLocation = potentialAncestor.getLocation();
82+
if (ancestorLocation != null && ancestorLocation.isPrefixOf(projectLocation)) {
83+
ancestors.add(potentialAncestor);
84+
}
85+
}
86+
87+
// Sort by path length (longer paths first = closer ancestors first)
88+
ancestors.sort((p1, p2) -> {
89+
IPath loc1 = p1.getLocation();
90+
IPath loc2 = p2.getLocation();
91+
if (loc1 == null || loc2 == null) {
92+
return 0;
93+
}
94+
// Reverse order: longer paths (closer ancestors) come first
95+
return Integer.compare(loc2.segmentCount(), loc1.segmentCount());
96+
});
97+
98+
return Collections.unmodifiableList(ancestors);
99+
}
100+
101+
/**
102+
* Clears the cache for the given project.
103+
*
104+
* @param project the project whose cache entry should be cleared
105+
*/
106+
public void clearCache(IProject project) {
107+
nestingCache.remove(project);
108+
}
109+
110+
/**
111+
* Clears the entire cache. Should be called when projects are created, deleted, moved, or closed.
112+
*/
113+
public void clearCache() {
114+
nestingCache.clear();
115+
}
116+
}

resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectPreferences.java

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.eclipse.core.runtime.jobs.MultiRule;
5656
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
5757
import org.eclipse.core.runtime.preferences.IExportedPreferences;
58+
import org.eclipse.core.runtime.preferences.InstanceScope;
5859
import org.eclipse.osgi.util.NLS;
5960
import org.osgi.service.prefs.BackingStoreException;
6061
import org.osgi.service.prefs.Preferences;
@@ -74,6 +75,12 @@ public class ProjectPreferences extends EclipsePreferences {
7475
* Cache which nodes have been loaded from disk
7576
*/
7677
private static final Set<String> loadedNodes = ConcurrentHashMap.newKeySet();
78+
79+
/**
80+
* Cache for project nesting relationships
81+
*/
82+
private static final ProjectNestingCache nestingCache = new ProjectNestingCache();
83+
7784
private IFile file;
7885
private volatile boolean initialized;
7986
/**
@@ -145,6 +152,8 @@ static void deleted(IFolder folder) throws CoreException {
145152
boolean hasResourcesSettings = getFile(folder, PREFS_REGULAR_QUALIFIER).exists() || getFile(folder, PREFS_DERIVED_QUALIFIER).exists();
146153
// remove the preferences
147154
removeNode(projectNode);
155+
// Clear nesting cache as project structure changed
156+
nestingCache.clearCache();
148157
// notifies the CharsetManager
149158
if (hasResourcesSettings) {
150159
preferencesChanged(folder.getProject());
@@ -165,6 +174,8 @@ static void deleted(IProject project) throws CoreException {
165174
boolean hasResourcesSettings = getFile(project, PREFS_REGULAR_QUALIFIER).exists() || getFile(project, PREFS_DERIVED_QUALIFIER).exists();
166175
// remove the preferences
167176
removeNode(projectNode);
177+
// Clear nesting cache as project was deleted
178+
nestingCache.clearCache();
168179
// notifies the CharsetManager
169180
if (hasResourcesSettings) {
170181
preferencesChanged(project);
@@ -613,11 +624,97 @@ protected void load() throws BackingStoreException {
613624
}
614625

615626
private void load(boolean reportProblems) throws BackingStoreException {
627+
// Load hierarchical preferences if enabled
628+
if (isHierarchicalPreferencesEnabled()) {
629+
loadHierarchical(reportProblems);
630+
} else {
631+
loadSingle(reportProblems);
632+
}
633+
}
634+
635+
/**
636+
* Checks if hierarchical project preferences are enabled.
637+
*/
638+
private boolean isHierarchicalPreferencesEnabled() {
639+
if (project == null || qualifier == null) {
640+
return false;
641+
}
642+
Preferences node = Platform.getPreferencesService().getRootNode()
643+
.node(InstanceScope.SCOPE)
644+
.node(ResourcesPlugin.PI_RESOURCES);
645+
return node.getBoolean(ResourcesPlugin.PREF_ENABLE_HIERARCHICAL_PROJECT_PREFERENCES,
646+
ResourcesPlugin.DEFAULT_PREF_ENABLE_HIERARCHICAL_PROJECT_PREFERENCES);
647+
}
648+
649+
/**
650+
* Loads preferences hierarchically from ancestor projects.
651+
*/
652+
private void loadHierarchical(boolean reportProblems) throws BackingStoreException {
653+
List<IProject> ancestors = nestingCache.getAncestorProjects(project, getWorkspace());
654+
655+
// Load from ancestors first (root to immediate parent)
656+
for (int i = ancestors.size() - 1; i >= 0; i--) {
657+
IProject ancestor = ancestors.get(i);
658+
IFile ancestorFile = getFile(ancestor, qualifier);
659+
if (ancestorFile != null && ancestorFile.exists()) {
660+
loadPropertiesFromFile(ancestorFile, reportProblems);
661+
}
662+
}
663+
664+
// Finally, load from the project itself (which will override ancestor values)
665+
loadSingle(reportProblems);
666+
// Mark as loaded even if the local file doesn't exist (we may have inherited values)
667+
loadedNodes.add(absolutePath());
668+
}
669+
670+
/**
671+
* Loads properties from a single file into this node.
672+
*/
673+
private void loadPropertiesFromFile(IFile file, boolean reportProblems) throws BackingStoreException {
674+
if (file == null || !file.exists()) {
675+
return;
676+
}
677+
if (Policy.DEBUG_PREFERENCES) {
678+
Policy.debug("Loading hierarchical preferences from file: " + file.getFullPath()); //$NON-NLS-1$
679+
}
680+
Properties fromDisk = new Properties();
681+
try (InputStream input = file.getContents(true)) {
682+
fromDisk.load(input);
683+
convertFromProperties(this, fromDisk, true);
684+
} catch (CoreException e) {
685+
if (e.getStatus().getCode() == IResourceStatus.RESOURCE_NOT_FOUND) {
686+
if (Policy.DEBUG_PREFERENCES) {
687+
Policy.debug("Hierarchical preference file does not exist: " + file.getFullPath()); //$NON-NLS-1$
688+
}
689+
return;
690+
}
691+
if (reportProblems) {
692+
String message = NLS.bind(Messages.preferences_loadException, file.getFullPath());
693+
log(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e));
694+
throw new BackingStoreException(message);
695+
}
696+
} catch (IOException e) {
697+
if (reportProblems) {
698+
String message = NLS.bind(Messages.preferences_loadException, file.getFullPath());
699+
log(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e));
700+
throw new BackingStoreException(message);
701+
}
702+
}
703+
}
704+
705+
/**
706+
* Loads preferences from a single file (non-hierarchical).
707+
*/
708+
private void loadSingle(boolean reportProblems) throws BackingStoreException {
616709
IFile localFile = getFile();
617710
if (localFile == null || !localFile.exists()) {
618711
if (Policy.DEBUG_PREFERENCES) {
619712
Policy.debug("Unable to determine preference file or file does not exist for node: " + absolutePath()); //$NON-NLS-1$
620713
}
714+
// Mark as loaded even if file doesn't exist when in non-hierarchical mode
715+
if (!isHierarchicalPreferencesEnabled()) {
716+
loadedNodes.add(absolutePath());
717+
}
621718
return;
622719
}
623720
if (Policy.DEBUG_PREFERENCES) {
@@ -627,7 +724,10 @@ private void load(boolean reportProblems) throws BackingStoreException {
627724
try (InputStream input = localFile.getContents(true)) {
628725
fromDisk.load(input);
629726
convertFromProperties(this, fromDisk, true);
630-
loadedNodes.add(absolutePath());
727+
// Mark as loaded only in non-hierarchical mode (hierarchical mode marks at the end)
728+
if (!isHierarchicalPreferencesEnabled()) {
729+
loadedNodes.add(absolutePath());
730+
}
631731
} catch (CoreException e) {
632732
if (e.getStatus().getCode() == IResourceStatus.RESOURCE_NOT_FOUND) {
633733
if (Policy.DEBUG_PREFERENCES) {

resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/ResourcesPlugin.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,23 @@ public final class ResourcesPlugin extends Plugin {
368368
*/
369369
public static final boolean DEFAULT_PREF_SEPARATE_DERIVED_ENCODINGS = false;
370370

371+
/**
372+
* Name of a preference for enabling hierarchical project preferences.
373+
* When enabled, project preferences are searched up in the chain of nested projects,
374+
* where a project A is nested within a project B if B.getLocation().isPrefixOf(A.getLocation()).
375+
* Values of deeper nested projects overwrite values in lower level projects.
376+
*
377+
* @since 3.20
378+
*/
379+
public static final String PREF_ENABLE_HIERARCHICAL_PROJECT_PREFERENCES = "enableHierarchicalProjectPreferences"; //$NON-NLS-1$
380+
381+
/**
382+
* Default setting for {@value #PREF_ENABLE_HIERARCHICAL_PROJECT_PREFERENCES}.
383+
*
384+
* @since 3.20
385+
*/
386+
public static final boolean DEFAULT_PREF_ENABLE_HIERARCHICAL_PROJECT_PREFERENCES = true;
387+
371388
/**
372389
* Name of a preference for configuring the marker severity in case project
373390
* description references an unknown nature.

0 commit comments

Comments
 (0)