22
33import java .util .ArrayList ;
44import java .util .List ;
5+ import java .util .Map ;
6+ import java .util .concurrent .ConcurrentHashMap ;
57
68import org .eclipse .core .resources .IProject ;
9+ import org .eclipse .core .resources .IResource ;
10+ import org .eclipse .core .resources .IResourceChangeEvent ;
11+ import org .eclipse .core .resources .IResourceChangeListener ;
12+ import org .eclipse .core .resources .IResourceDelta ;
13+ import org .eclipse .core .resources .IResourceDeltaVisitor ;
714import org .eclipse .core .resources .IWorkspaceRoot ;
815import org .eclipse .core .resources .ResourcesPlugin ;
16+ import org .eclipse .core .runtime .CoreException ;
917import org .eclipse .core .runtime .IPath ;
1018import org .eclipse .core .runtime .IProgressMonitor ;
19+ import org .eclipse .jdt .core .ElementChangedEvent ;
1120import org .eclipse .jdt .core .IClasspathEntry ;
21+ import org .eclipse .jdt .core .IElementChangedListener ;
22+ import org .eclipse .jdt .core .IJavaElement ;
23+ import org .eclipse .jdt .core .IJavaElementDelta ;
1224import org .eclipse .jdt .core .IJavaProject ;
1325import org .eclipse .jdt .core .JavaCore ;
1426import org .eclipse .jdt .core .JavaModelException ;
1830import com .microsoft .jdtls .ext .core .JdtlsExtActivator ;
1931
2032public class ProjectResolver {
33+
34+ // Cache for project dependency information
35+ private static final Map <String , CachedDependencyInfo > dependencyCache = new ConcurrentHashMap <>();
36+
37+ // Flag to track if listeners are registered
38+ private static volatile boolean listenersRegistered = false ;
39+
40+ // Lock for listener registration
41+ private static final Object listenerLock = new Object ();
42+
43+ /**
44+ * Cached dependency information with timestamp
45+ */
46+ private static class CachedDependencyInfo {
47+ final List <DependencyInfo > dependencies ;
48+ final long timestamp ;
49+ final long classpathHash ;
50+
51+ CachedDependencyInfo (List <DependencyInfo > dependencies , long classpathHash ) {
52+ this .dependencies = new ArrayList <>(dependencies );
53+ this .timestamp = System .currentTimeMillis ();
54+ this .classpathHash = classpathHash ;
55+ }
56+
57+ boolean isValid () {
58+ // Cache is valid for 5 minutes
59+ return (System .currentTimeMillis () - timestamp ) < 300000 ;
60+ }
61+ }
62+
63+ /**
64+ * Listener for Java element changes (classpath changes, project references, etc.)
65+ */
66+ private static final IElementChangedListener javaElementListener = new IElementChangedListener () {
67+ @ Override
68+ public void elementChanged (ElementChangedEvent event ) {
69+ IJavaElementDelta delta = event .getDelta ();
70+ processDelta (delta );
71+ }
72+
73+ private void processDelta (IJavaElementDelta delta ) {
74+ IJavaElement element = delta .getElement ();
75+ int flags = delta .getFlags ();
76+
77+ // Check for classpath changes
78+ if ((flags & IJavaElementDelta .F_CLASSPATH_CHANGED ) != 0 ||
79+ (flags & IJavaElementDelta .F_RESOLVED_CLASSPATH_CHANGED ) != 0 ) {
80+
81+ if (element instanceof IJavaProject ) {
82+ IJavaProject project = (IJavaProject ) element ;
83+ invalidateCache (project .getProject ());
84+ }
85+ }
86+
87+ // Recursively process children
88+ for (IJavaElementDelta child : delta .getAffectedChildren ()) {
89+ processDelta (child );
90+ }
91+ }
92+ };
93+
94+ /**
95+ * Listener for resource changes (pom.xml, build.gradle, etc.)
96+ */
97+ private static final IResourceChangeListener resourceListener = new IResourceChangeListener () {
98+ @ Override
99+ public void resourceChanged (IResourceChangeEvent event ) {
100+ if (event .getType () != IResourceChangeEvent .POST_CHANGE ) {
101+ return ;
102+ }
103+
104+ IResourceDelta delta = event .getDelta ();
105+ if (delta == null ) {
106+ return ;
107+ }
108+
109+ try {
110+ delta .accept (new IResourceDeltaVisitor () {
111+ @ Override
112+ public boolean visit (IResourceDelta delta ) throws CoreException {
113+ IResource resource = delta .getResource ();
114+
115+ // Check for build file changes
116+ if (resource .getType () == IResource .FILE ) {
117+ String fileName = resource .getName ();
118+ if ("pom.xml" .equals (fileName ) ||
119+ "build.gradle" .equals (fileName ) ||
120+ "build.gradle.kts" .equals (fileName ) ||
121+ ".classpath" .equals (fileName ) ||
122+ ".project" .equals (fileName )) {
123+
124+ IProject project = resource .getProject ();
125+ if (project != null ) {
126+ invalidateCache (project );
127+ }
128+ }
129+ }
130+ return true ;
131+ }
132+ });
133+ } catch (CoreException e ) {
134+ JdtlsExtActivator .logException ("Error processing resource delta" , e );
135+ }
136+ }
137+ };
138+
139+ /**
140+ * Initialize listeners for cache invalidation
141+ */
142+ private static void ensureListenersRegistered () {
143+ if (!listenersRegistered ) {
144+ synchronized (listenerLock ) {
145+ if (!listenersRegistered ) {
146+ try {
147+ // Register Java element change listener
148+ JavaCore .addElementChangedListener (javaElementListener ,
149+ ElementChangedEvent .POST_CHANGE );
150+
151+ // Register resource change listener
152+ ResourcesPlugin .getWorkspace ().addResourceChangeListener (
153+ resourceListener ,
154+ IResourceChangeEvent .POST_CHANGE );
155+
156+ listenersRegistered = true ;
157+ JdtlsExtActivator .logInfo ("ProjectResolver cache listeners registered successfully" );
158+ } catch (Exception e ) {
159+ JdtlsExtActivator .logException ("Failed to register ProjectResolver listeners" , e );
160+ }
161+ }
162+ }
163+ }
164+ }
165+
166+ /**
167+ * Invalidate cache for a specific project
168+ */
169+ private static void invalidateCache (IProject project ) {
170+ if (project == null ) {
171+ return ;
172+ }
173+
174+ String projectPath = project .getLocation () != null ?
175+ project .getLocation ().toOSString () : project .getName ();
176+
177+ if (dependencyCache .remove (projectPath ) != null ) {
178+ JdtlsExtActivator .logInfo ("Cache invalidated for project: " + project .getName ());
179+ }
180+ }
181+
182+ /**
183+ * Clear all cached dependency information
184+ */
185+ public static void clearCache () {
186+ dependencyCache .clear ();
187+ JdtlsExtActivator .logInfo ("ProjectResolver cache cleared" );
188+ }
189+
190+ /**
191+ * Calculate a simple hash of classpath entries for cache validation
192+ */
193+ private static long calculateClasspathHash (IJavaProject javaProject ) {
194+ try {
195+ IClasspathEntry [] entries = javaProject .getResolvedClasspath (true );
196+ long hash = 0 ;
197+ for (IClasspathEntry entry : entries ) {
198+ hash = hash * 31 + entry .getPath ().toString ().hashCode ();
199+ hash = hash * 31 + entry .getEntryKind ();
200+ }
201+ return hash ;
202+ } catch (JavaModelException e ) {
203+ return 0 ;
204+ }
205+ }
21206
22207 // Constants for dependency info keys
23208 private static final String KEY_BUILD_TOOL = "buildTool" ;
@@ -44,30 +229,48 @@ public DependencyInfo(String key, String value) {
44229
45230 /**
46231 * Resolve project dependencies information including JDK version.
232+ * Supports both single projects and multi-module aggregator projects.
47233 *
48- * @param projectUri The project URI
234+ * @param fileUri The file URI
49235 * @param monitor Progress monitor for cancellation support
50236 * @return List of DependencyInfo containing key-value pairs of project information
51237 */
52- public static List <DependencyInfo > resolveProjectDependencies (String projectUri , IProgressMonitor monitor ) {
238+ public static List <DependencyInfo > resolveProjectDependencies (String fileUri , IProgressMonitor monitor ) {
239+ // Ensure listeners are registered for cache invalidation
240+ ensureListenersRegistered ();
241+
53242 List <DependencyInfo > result = new ArrayList <>();
54243
55244 try {
56- IPath projectPath = ResourceUtils .canonicalFilePathFromURI (projectUri );
245+ IPath fileIPath = ResourceUtils .canonicalFilePathFromURI (fileUri );
57246
58247 // Find the project
59248 IWorkspaceRoot root = ResourcesPlugin .getWorkspace ().getRoot ();
60- IProject project = findProjectByPath (root , projectPath );
249+ IProject project = findProjectByPath (root , fileIPath );
61250
62251 if (project == null || !project .isAccessible ()) {
63252 return result ;
64253 }
65254
66255 IJavaProject javaProject = JavaCore .create (project );
256+ // Check if this is a Java project
67257 if (javaProject == null || !javaProject .exists ()) {
68258 return result ;
69259 }
70260
261+ // Generate cache key based on project location
262+ String cacheKey = project .getLocation ().toOSString ();
263+
264+ // Calculate current classpath hash for validation
265+ long currentClasspathHash = calculateClasspathHash (javaProject );
266+
267+ // Try to get from cache
268+ CachedDependencyInfo cached = dependencyCache .get (cacheKey );
269+ if (cached != null && cached .isValid () && cached .classpathHash == currentClasspathHash ) {
270+ JdtlsExtActivator .logInfo ("Using cached dependencies for project: " + project .getName ());
271+ return new ArrayList <>(cached .dependencies );
272+ }
273+
71274 // Add basic project information
72275 addBasicProjectInfo (result , project , javaProject );
73276
@@ -76,6 +279,9 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
76279
77280 // Add build tool info by checking for build files
78281 detectBuildTool (result , project );
282+
283+ // Store in cache
284+ dependencyCache .put (cacheKey , new CachedDependencyInfo (result , currentClasspathHash ));
79285
80286 } catch (Exception e ) {
81287 JdtlsExtActivator .logException ("Error in resolveProjectDependencies" , e );
@@ -86,14 +292,31 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
86292
87293 /**
88294 * Find project by path from all projects in workspace.
295+ * The path can be either a project root path or a file/folder path within a project.
296+ * This method will find the project that contains the given path.
297+ *
298+ * @param root The workspace root
299+ * @param filePath The path to search for (can be project root or file within project)
300+ * @return The project that contains the path, or null if not found
89301 */
90- private static IProject findProjectByPath (IWorkspaceRoot root , IPath projectPath ) {
302+ private static IProject findProjectByPath (IWorkspaceRoot root , IPath filePath ) {
91303 IProject [] allProjects = root .getProjects ();
304+
305+ // First pass: check for exact project location match (most efficient)
92306 for (IProject p : allProjects ) {
93- if (p .getLocation () != null && p .getLocation ().equals (projectPath )) {
307+ if (p .getLocation () != null && p .getLocation ().equals (filePath )) {
94308 return p ;
95309 }
96310 }
311+
312+ // Second pass: check if the file path is within any project directory
313+ // This handles cases where filePath points to a file or folder inside a project
314+ for (IProject p : allProjects ) {
315+ if (p .getLocation () != null && p .getLocation ().isPrefixOf (filePath )) {
316+ return p ;
317+ }
318+ }
319+
97320 return null ;
98321 }
99322
@@ -158,17 +381,19 @@ private static void processClasspathEntries(List<DependencyInfo> result, IJavaPr
158381
159382 /**
160383 * Process a library classpath entry.
384+ * Only returns the library file name without full path to reduce data size.
161385 */
162386 private static void processLibraryEntry (List <DependencyInfo > result , IClasspathEntry entry , int libCount ) {
163387 IPath libPath = entry .getPath ();
164388 if (libPath != null ) {
165- result . add ( new DependencyInfo ( "library_" + libCount ,
166- libPath . lastSegment () + " (" + libPath .toOSString () + ")" ));
389+ // Only keep the file name, remove the full path
390+ result . add ( new DependencyInfo ( "library_" + libCount , libPath .lastSegment () ));
167391 }
168392 }
169393
170394 /**
171395 * Process a project reference classpath entry.
396+ * Simplified to only extract essential information.
172397 */
173398 private static void processProjectEntry (List <DependencyInfo > result , IClasspathEntry entry , int projectRefCount ) {
174399 IPath projectRefPath = entry .getPath ();
@@ -185,12 +410,21 @@ private static void processContainerEntry(List<DependencyInfo> result, IClasspat
185410 String containerPath = entry .getPath ().toString ();
186411
187412 if (containerPath .contains ("JRE_CONTAINER" )) {
188- result . add ( new DependencyInfo ( KEY_JRE_CONTAINER_PATH , containerPath ));
413+ // Only extract the JRE version, not the full container path
189414 try {
190415 String vmInstallName = JavaRuntime .getVMInstallName (entry .getPath ());
191416 addIfNotNull (result , KEY_JRE_CONTAINER , vmInstallName );
192417 } catch (Exception e ) {
193- // Ignore if unable to get VM install name
418+ // Fallback: try to extract version from path
419+ if (containerPath .contains ("JavaSE-" )) {
420+ int startIdx = containerPath .lastIndexOf ("JavaSE-" );
421+ String version = containerPath .substring (startIdx );
422+ // Clean up any trailing characters
423+ if (version .contains ("/" )) {
424+ version = version .substring (0 , version .indexOf ("/" ));
425+ }
426+ result .add (new DependencyInfo (KEY_JRE_CONTAINER , version ));
427+ }
194428 }
195429 } else if (containerPath .contains ("MAVEN" )) {
196430 result .add (new DependencyInfo (KEY_BUILD_TOOL , "Maven" ));
0 commit comments