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 ;
79import org .eclipse .core .resources .IWorkspaceRoot ;
810import org .eclipse .core .resources .ResourcesPlugin ;
11+ import org .eclipse .core .runtime .CoreException ;
912import org .eclipse .core .runtime .IPath ;
1013import org .eclipse .core .runtime .IProgressMonitor ;
1114import org .eclipse .jdt .core .IClasspathEntry ;
@@ -44,6 +47,7 @@ public DependencyInfo(String key, String value) {
4447
4548 /**
4649 * Resolve project dependencies information including JDK version.
50+ * Supports both single projects and multi-module aggregator projects.
4751 *
4852 * @param projectUri The project URI
4953 * @param monitor Progress monitor for cancellation support
@@ -64,8 +68,13 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
6468 }
6569
6670 IJavaProject javaProject = JavaCore .create (project );
71+ // Check if this is a Java project
6772 if (javaProject == null || !javaProject .exists ()) {
68- return result ;
73+ // Not a Java project - might be an aggregator/parent project
74+ // Try to find Java sub-projects under this path
75+ JdtlsExtActivator .logInfo ("Not a Java project: " + project .getName () +
76+ ", checking for sub-projects" );
77+ return resolveAggregatorProjectDependencies (root , projectPath , monitor );
6978 }
7079
7180 // Add basic project information
@@ -83,6 +92,210 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
8392
8493 return result ;
8594 }
95+
96+ /**
97+ * Resolve dependencies for an aggregator/parent project by finding and processing all Java sub-projects.
98+ * This handles multi-module Maven/Gradle projects where the parent is not a Java project itself.
99+ * Returns aggregated information useful for AI context (Java version, common dependencies, build tool).
100+ *
101+ * @param root The workspace root
102+ * @param parentPath The path of the parent/aggregator project
103+ * @param monitor Progress monitor
104+ * @return Aggregated dependency information from all sub-projects
105+ */
106+ private static List <DependencyInfo > resolveAggregatorProjectDependencies (
107+ IWorkspaceRoot root , IPath parentPath , IProgressMonitor monitor ) {
108+
109+ List <DependencyInfo > result = new ArrayList <>();
110+ List <IJavaProject > javaProjects = new ArrayList <>();
111+
112+ // Find all Java projects under the parent path
113+ IProject [] allProjects = root .getProjects ();
114+ for (IProject p : allProjects ) {
115+ if (p .getLocation () != null && parentPath .isPrefixOf (p .getLocation ())) {
116+ try {
117+ if (p .isAccessible () && p .hasNature (JavaCore .NATURE_ID )) {
118+ IJavaProject jp = JavaCore .create (p );
119+ if (jp != null && jp .exists ()) {
120+ javaProjects .add (jp );
121+ }
122+ }
123+ } catch (CoreException e ) {
124+ // Skip this project
125+ }
126+ }
127+ }
128+
129+ if (javaProjects .isEmpty ()) {
130+ JdtlsExtActivator .logInfo ("No Java sub-projects found under: " + parentPath .toOSString ());
131+ return result ;
132+ }
133+
134+ JdtlsExtActivator .logInfo ("Found " + javaProjects .size () +
135+ " Java sub-project(s) under: " + parentPath .toOSString ());
136+
137+ // Mark as aggregator project
138+ result .add (new DependencyInfo ("aggregatorProject" , "true" ));
139+ result .add (new DependencyInfo ("totalSubProjects" , String .valueOf (javaProjects .size ())));
140+
141+ // Collect sub-project names for reference
142+ StringBuilder projectNames = new StringBuilder ();
143+ for (int i = 0 ; i < javaProjects .size (); i ++) {
144+ if (i > 0 ) projectNames .append (", " );
145+ projectNames .append (javaProjects .get (i ).getProject ().getName ());
146+ }
147+ result .add (new DependencyInfo ("subProjectNames" , projectNames .toString ()));
148+
149+ // Determine the primary/representative Java version (most common or highest)
150+ String primaryJavaVersion = determinePrimaryJavaVersion (javaProjects );
151+ if (primaryJavaVersion != null ) {
152+ result .add (new DependencyInfo (KEY_JAVA_VERSION , primaryJavaVersion ));
153+ }
154+
155+ // Collect all unique libraries across sub-projects (top 10 most common)
156+ Map <String , Integer > libraryFrequency = collectLibraryFrequency (javaProjects , monitor );
157+ addTopLibraries (result , libraryFrequency , 10 );
158+
159+ // Detect build tool from parent directory
160+ IProject parentProject = findProjectByPath (root , parentPath );
161+ if (parentProject != null ) {
162+ detectBuildTool (result , parentProject );
163+ }
164+
165+ // Get JRE container info from first sub-project (usually consistent across modules)
166+ if (!javaProjects .isEmpty ()) {
167+ extractJreInfo (result , javaProjects .get (0 ));
168+ }
169+
170+ return result ;
171+ }
172+
173+ /**
174+ * Determine the primary Java version from all sub-projects.
175+ * Returns the most common version, or the highest if there's a tie.
176+ */
177+ private static String determinePrimaryJavaVersion (List <IJavaProject > javaProjects ) {
178+ Map <String , Integer > versionCount = new ConcurrentHashMap <>();
179+
180+ for (IJavaProject jp : javaProjects ) {
181+ String version = jp .getOption (JavaCore .COMPILER_COMPLIANCE , true );
182+ if (version != null ) {
183+ versionCount .put (version , versionCount .getOrDefault (version , 0 ) + 1 );
184+ }
185+ }
186+
187+ if (versionCount .isEmpty ()) {
188+ return null ;
189+ }
190+
191+ // Find most common version (or highest if tie)
192+ return versionCount .entrySet ().stream ()
193+ .max ((e1 , e2 ) -> {
194+ int countCompare = Integer .compare (e1 .getValue (), e2 .getValue ());
195+ if (countCompare != 0 ) return countCompare ;
196+ // If same count, prefer higher version
197+ return e1 .getKey ().compareTo (e2 .getKey ());
198+ })
199+ .map (Map .Entry ::getKey )
200+ .orElse (null );
201+ }
202+
203+ /**
204+ * Collect frequency of all libraries across sub-projects.
205+ * Returns a map of library name to frequency count.
206+ */
207+ private static Map <String , Integer > collectLibraryFrequency (
208+ List <IJavaProject > javaProjects , IProgressMonitor monitor ) {
209+
210+ Map <String , Integer > libraryFrequency = new ConcurrentHashMap <>();
211+
212+ for (IJavaProject jp : javaProjects ) {
213+ if (monitor .isCanceled ()) {
214+ break ;
215+ }
216+
217+ try {
218+ IClasspathEntry [] entries = jp .getResolvedClasspath (true );
219+ for (IClasspathEntry entry : entries ) {
220+ if (entry .getEntryKind () == IClasspathEntry .CPE_LIBRARY ) {
221+ IPath libPath = entry .getPath ();
222+ if (libPath != null ) {
223+ String libName = libPath .lastSegment ();
224+ libraryFrequency .put (libName ,
225+ libraryFrequency .getOrDefault (libName , 0 ) + 1 );
226+ }
227+ }
228+ }
229+ } catch (JavaModelException e ) {
230+ // Skip this project
231+ }
232+ }
233+
234+ return libraryFrequency ;
235+ }
236+
237+ /**
238+ * Add top N most common libraries to result.
239+ */
240+ private static void addTopLibraries (List <DependencyInfo > result ,
241+ Map <String , Integer > libraryFrequency , int topN ) {
242+
243+ if (libraryFrequency .isEmpty ()) {
244+ result .add (new DependencyInfo (KEY_TOTAL_LIBRARIES , "0" ));
245+ return ;
246+ }
247+
248+ // Sort by frequency (descending) and take top N
249+ List <Map .Entry <String , Integer >> topLibs = libraryFrequency .entrySet ().stream ()
250+ .sorted ((e1 , e2 ) -> Integer .compare (e2 .getValue (), e1 .getValue ()))
251+ .limit (topN )
252+ .collect (java .util .stream .Collectors .toList ());
253+
254+ result .add (new DependencyInfo (KEY_TOTAL_LIBRARIES ,
255+ String .valueOf (libraryFrequency .size ())));
256+
257+ // Add top common libraries
258+ int index = 1 ;
259+ for (Map .Entry <String , Integer > entry : topLibs ) {
260+ result .add (new DependencyInfo ("commonLibrary_" + index ,
261+ entry .getKey () + " (used in " + entry .getValue () + " modules)" ));
262+ index ++;
263+ }
264+ }
265+
266+ /**
267+ * Extract JRE container information from a Java project.
268+ */
269+ private static void extractJreInfo (List <DependencyInfo > result , IJavaProject javaProject ) {
270+ try {
271+ IClasspathEntry [] entries = javaProject .getResolvedClasspath (true );
272+ for (IClasspathEntry entry : entries ) {
273+ if (entry .getEntryKind () == IClasspathEntry .CPE_CONTAINER ) {
274+ String containerPath = entry .getPath ().toString ();
275+ if (containerPath .contains ("JRE_CONTAINER" )) {
276+ try {
277+ String vmInstallName = JavaRuntime .getVMInstallName (entry .getPath ());
278+ addIfNotNull (result , KEY_JRE_CONTAINER , vmInstallName );
279+ return ;
280+ } catch (Exception e ) {
281+ // Fallback: extract from path
282+ if (containerPath .contains ("JavaSE-" )) {
283+ int startIdx = containerPath .lastIndexOf ("JavaSE-" );
284+ String version = containerPath .substring (startIdx );
285+ if (version .contains ("/" )) {
286+ version = version .substring (0 , version .indexOf ("/" ));
287+ }
288+ result .add (new DependencyInfo (KEY_JRE_CONTAINER , version ));
289+ return ;
290+ }
291+ }
292+ }
293+ }
294+ }
295+ } catch (JavaModelException e ) {
296+ // Ignore
297+ }
298+ }
86299
87300 /**
88301 * Find project by path from all projects in workspace.
@@ -158,17 +371,19 @@ private static void processClasspathEntries(List<DependencyInfo> result, IJavaPr
158371
159372 /**
160373 * Process a library classpath entry.
374+ * Only returns the library file name without full path to reduce data size.
161375 */
162376 private static void processLibraryEntry (List <DependencyInfo > result , IClasspathEntry entry , int libCount ) {
163377 IPath libPath = entry .getPath ();
164378 if (libPath != null ) {
165- result . add ( new DependencyInfo ( "library_" + libCount ,
166- libPath . lastSegment () + " (" + libPath .toOSString () + ")" ));
379+ // Only keep the file name, remove the full path
380+ result . add ( new DependencyInfo ( "library_" + libCount , libPath .lastSegment () ));
167381 }
168382 }
169383
170384 /**
171385 * Process a project reference classpath entry.
386+ * Simplified to only extract essential information.
172387 */
173388 private static void processProjectEntry (List <DependencyInfo > result , IClasspathEntry entry , int projectRefCount ) {
174389 IPath projectRefPath = entry .getPath ();
@@ -185,12 +400,21 @@ private static void processContainerEntry(List<DependencyInfo> result, IClasspat
185400 String containerPath = entry .getPath ().toString ();
186401
187402 if (containerPath .contains ("JRE_CONTAINER" )) {
188- result . add ( new DependencyInfo ( KEY_JRE_CONTAINER_PATH , containerPath ));
403+ // Only extract the JRE version, not the full container path
189404 try {
190405 String vmInstallName = JavaRuntime .getVMInstallName (entry .getPath ());
191406 addIfNotNull (result , KEY_JRE_CONTAINER , vmInstallName );
192407 } catch (Exception e ) {
193- // Ignore if unable to get VM install name
408+ // Fallback: try to extract version from path
409+ if (containerPath .contains ("JavaSE-" )) {
410+ int startIdx = containerPath .lastIndexOf ("JavaSE-" );
411+ String version = containerPath .substring (startIdx );
412+ // Clean up any trailing characters
413+ if (version .contains ("/" )) {
414+ version = version .substring (0 , version .indexOf ("/" ));
415+ }
416+ result .add (new DependencyInfo (KEY_JRE_CONTAINER , version ));
417+ }
194418 }
195419 } else if (containerPath .contains ("MAVEN" )) {
196420 result .add (new DependencyInfo (KEY_BUILD_TOOL , "Maven" ));
0 commit comments