@@ -6204,12 +6204,16 @@ def _get_raw_stacks_from_api(self):
62046204 return list(self._client.provider.get_web_app_stacks(stack_os_type=None))
62056205
62066206 def _parse_raw_stacks(self, stacks):
6207+ # Track seen runtime display names to avoid duplicates in Linux parsing.
6208+ # Linux Java containers (e.g., JBOSSEAP) can produce duplicate entries across major versions.
6209+ # Windows parsing doesn't have this issue due to its different structure.
6210+ seen_runtimes = set()
62076211 for lang in stacks:
62086212 for major_version in lang.major_versions:
62096213 if self._linux:
62106214 if lang.display_text.lower() == "java":
62116215 continue
6212- self._parse_major_version_linux(major_version, self._stacks)
6216+ self._parse_major_version_linux(major_version, self._stacks, seen_runtimes )
62136217 if self._windows:
62146218 self._parse_major_version_windows(major_version, self._stacks, self.windows_config_mappings)
62156219
@@ -6354,17 +6358,116 @@ def _filter(minor_version):
63546358 return cls._is_valid_runtime_setting(cls._get_runtime_setting(minor_version, linux, java))
63556359 return [m for m in major_version.minor_versions if _filter(m)]
63566360
6361+ @staticmethod
6362+ def _java_version_sort_key(version):
6363+ """Sort key for Java versions. Handles formats like "25", "1.8", "11.0", etc.
6364+ Returns a negative integer representing the version, so sorted() produces descending order (newest first)."""
6365+ if version == "1.8":
6366+ return -8 # Treat 1.8 as Java 8
6367+ if version.startswith("1."):
6368+ # Handle legacy "1.x" format (e.g., "1.7", "1.9")
6369+ try:
6370+ return -int(version.split('.')[1])
6371+ except (IndexError, ValueError):
6372+ return 0
6373+ # Handle "X.Y" format (e.g., "11.0", "17.0") or plain integers ("25", "21")
6374+ try:
6375+ return -int(version.split('.')[0])
6376+ except ValueError:
6377+ return 0
6378+
6379+ @staticmethod
6380+ def _get_java_versions_from_minor_versions(minor_versions):
6381+ """Dynamically extract unique Java versions from minor version values.
6382+ Used for Linux Java SE containers where minor.value is like "25.0.0", "21.0.0".
6383+ Returns versions sorted in descending order (newest first)."""
6384+ java_versions = set()
6385+ for minor in minor_versions:
6386+ # minor.value is like "25.0.0", "21.0.0", "17.0.0", "11.0.0", "8.0.0" or "1.8.0"
6387+ value = minor.value
6388+ if value:
6389+ # Handle both "1.8" format and newer "25", "21" formats
6390+ if value.startswith("1.8"):
6391+ java_versions.add("1.8")
6392+ else:
6393+ # Extract major version number (e.g., "25" from "25.0.0")
6394+ major_ver = value.split('.')[0]
6395+ if major_ver.isdigit():
6396+ java_versions.add(major_ver)
6397+ # Sort descending (newest versions first)
6398+ return sorted(java_versions, key=_StackRuntimeHelper._java_version_sort_key)
6399+
6400+ @staticmethod
6401+ def _get_java_versions_from_windows_container(container_settings):
6402+ """Dynamically extract Java versions from Windows container settings.
6403+ Looks at the 'runtimes' array in additional_properties.
6404+ Returns versions sorted in descending order (newest first)."""
6405+ java_versions = set()
6406+ additional_props = getattr(container_settings, 'additional_properties', {}) or {}
6407+ runtimes_array = additional_props.get('runtimes', [])
6408+
6409+ for runtime_info in runtimes_array:
6410+ version = runtime_info.get('runtimeVersion')
6411+ if version:
6412+ # Add version as-is (e.g., "25", "21", "17", "11", "1.8")
6413+ java_versions.add(version)
6414+
6415+ # Sort descending (newest versions first)
6416+ return sorted(java_versions, key=_StackRuntimeHelper._java_version_sort_key)
6417+
6418+ @staticmethod
6419+ def _get_java_runtimes_from_container_settings(container_settings):
6420+ """Dynamically extract Java runtimes from container settings.
6421+ Prefers the 'runtimes' array from the API when available (most future-proof),
6422+ falls back to individual java*Runtime properties in additional_properties,
6423+ and finally SDK-defined properties (java8_runtime, java11_runtime).
6424+ Returns list of tuples: (runtime_name, version, is_auto_update)"""
6425+ runtimes = []
6426+ is_auto_update = getattr(container_settings, 'is_auto_update', False)
6427+ additional_props = getattr(container_settings, 'additional_properties', {}) or {}
6428+
6429+ # Prefer the 'runtimes' array if available (cleanest, most future-proof)
6430+ runtimes_array = additional_props.get('runtimes', [])
6431+ if runtimes_array:
6432+ for runtime_info in runtimes_array:
6433+ runtime_name = runtime_info.get('runtime')
6434+ version = runtime_info.get('runtimeVersion')
6435+ if runtime_name and version:
6436+ runtimes.append((runtime_name, version, is_auto_update))
6437+ else:
6438+ # Fallback: Get runtimes from additional_properties (java*Runtime keys)
6439+ for key, value in additional_props.items():
6440+ # Match pattern like "java25Runtime", "java21Runtime", etc.
6441+ match = re.match(r'^java(\d+)Runtime$', key)
6442+ if match and value:
6443+ version = match.group(1)
6444+ runtimes.append((value, version, is_auto_update))
6445+
6446+ # Also get runtimes from SDK-defined properties (java8_runtime, java11_runtime)
6447+ if getattr(container_settings, 'java11_runtime', None):
6448+ # Avoid duplicates if already found in additional_properties
6449+ if not any(v == "11" for _, v, _ in runtimes):
6450+ runtimes.append((container_settings.java11_runtime, "11", is_auto_update))
6451+ if getattr(container_settings, 'java8_runtime', None):
6452+ if not any(v == "8" for _, v, _ in runtimes):
6453+ runtimes.append((container_settings.java8_runtime, "8", is_auto_update))
6454+
6455+ # Sort by version descending (newest first)
6456+ runtimes.sort(key=lambda x: _StackRuntimeHelper._java_version_sort_key(x[1]))
6457+ return runtimes
6458+
63576459 def _parse_major_version_windows(self, major_version, parsed_results, config_mappings):
63586460 java_container_minor_versions = self._get_valid_minor_versions(major_version, linux=False, java=True)
63596461 if java_container_minor_versions:
6360- javas = ["21", "17", "11", "1.8"]
6361- if len(java_container_minor_versions) > 0:
6362- leng = len(java_container_minor_versions) if len(java_container_minor_versions) < 3 else 3
6363- java_container_minor_versions = java_container_minor_versions[:leng]
63646462 for container in java_container_minor_versions:
63656463 container_settings = container.stack_settings.windows_container_settings
63666464 java_container = container_settings.java_container
63676465 container_version = container_settings.java_container_version
6466+ # Get Java versions from the container's runtimes array
6467+ javas = self._get_java_versions_from_windows_container(container_settings)
6468+ if not javas:
6469+ logger.debug("No Java versions found in Windows container settings for "
6470+ "container '%s' (version: '%s')", java_container, container_version)
63686471 for java in javas:
63696472 runtime = self.get_windows_java_runtime(
63706473 java,
@@ -6374,10 +6477,6 @@ def _parse_major_version_windows(self, major_version, parsed_results, config_map
63746477 parsed_results.append(runtime)
63756478 else:
63766479 minor_versions = self._get_valid_minor_versions(major_version, linux=False, java=False)
6377- if "Java" in major_version.display_text:
6378- if len(minor_versions) > 0:
6379- leng = len(minor_versions) if len(minor_versions) < 3 else 3
6380- minor_versions = minor_versions[1:leng]
63816480 for minor_version in minor_versions:
63826481 settings = minor_version.stack_settings.windows_runtime_settings
63836482 if "Java" not in minor_version.display_text:
@@ -6432,28 +6531,28 @@ def get_windows_java_runtime(self, java_version=None,
64326531 linux=False,
64336532 is_auto_update=is_auto_update)
64346533
6435- def _parse_major_version_linux(self, major_version, parsed_results):
6534+ def _parse_major_version_linux(self, major_version, parsed_results, seen_runtimes ):
64366535 minor_java_container_versions = self._get_valid_minor_versions(major_version, linux=True, java=True)
64376536 if "SE" in major_version.display_text:
6438- se_containers = [minor_java_container_versions[0]]
6439- for java in ["21", "17", "11", "1.8"]:
6537+ # Dynamically get Java versions from the available minor versions
6538+ java_versions = self._get_java_versions_from_minor_versions(minor_java_container_versions)
6539+ se_containers = [minor_java_container_versions[0]] if minor_java_container_versions else []
6540+ for java in java_versions:
64406541 se_java_containers = [c for c in minor_java_container_versions if c.value.startswith(java)]
6441- se_containers = se_containers + se_java_containers[:len(se_java_containers) if len(se_java_containers) < 2 else 2] # pylint: disable=line-too-long
6542+ se_containers = se_containers + se_java_containers
64426543 minor_java_container_versions = se_containers
64436544 if minor_java_container_versions:
6444- leng = len(minor_java_container_versions) if \
6445- len(minor_java_container_versions) < 3 else 3 if \
6446- "SE" not in major_version.display_text else len(minor_java_container_versions)
6447- for minor in minor_java_container_versions[:leng]:
6545+ for minor in minor_java_container_versions:
64486546 linux_container_settings = minor.stack_settings.linux_container_settings
6449- runtimes = [
6450- (linux_container_settings.additional_properties.get("java21Runtime"), "21", linux_container_settings.is_auto_update), # pylint: disable=line-too-long
6451- (linux_container_settings.additional_properties.get("java17Runtime"), "17", linux_container_settings.is_auto_update), # pylint: disable=line-too-long
6452- (linux_container_settings.java11_runtime, "11", linux_container_settings.is_auto_update),
6453- (linux_container_settings.java8_runtime, "8", linux_container_settings.is_auto_update)]
6454- # Remove the JBoss'_byol' entries from the output
6547+ # Dynamically get all Java runtimes from container settings
6548+ runtimes = self._get_java_runtimes_from_container_settings(linux_container_settings)
6549+ # Remove the 'JBoss _byol' entries from the output
64556550 runtimes = [(r, v, au) for (r, v, au) in runtimes if r is not None and not r.endswith("_byol")] # pylint: disable=line-too-long
64566551 for runtime_name, version, auto_update in [(r, v, au) for (r, v, au) in runtimes if r is not None]:
6552+ # Skip duplicates
6553+ if runtime_name in seen_runtimes:
6554+ continue
6555+ seen_runtimes.add(runtime_name)
64576556 runtime = self.Runtime(display_name=runtime_name,
64586557 configs={"linux_fx_version": runtime_name},
64596558 github_actions_properties={"github_actions_version": version},
0 commit comments