Skip to content

Commit 8203dae

Browse files
author
Trong Nhan Mai
committed
feat: add jdk version finder from maven central java artifacts
Signed-off-by: Trong Nhan Mai <[email protected]>
1 parent 95432bd commit 8203dae

File tree

2 files changed

+442
-0
lines changed

2 files changed

+442
-0
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved.
2+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
3+
4+
"""This module includes the functions for obtaining JDK version from a Java artifact."""
5+
6+
import logging
7+
import os
8+
import tempfile
9+
import urllib.parse
10+
import zipfile
11+
from enum import Enum
12+
13+
import requests
14+
15+
from macaron.artifact.maven import construct_maven_repository_path
16+
from macaron.config.global_config import global_config
17+
from macaron.errors import InvalidHTTPResponseError
18+
19+
logger: logging.Logger = logging.getLogger(__name__)
20+
21+
22+
class JavaArtifactExt(str, Enum):
23+
"""The extensions for Java artifacts."""
24+
25+
JAR = ".jar"
26+
27+
28+
def download_file(url: str, dest: str) -> None:
29+
"""Stream a file into a local destination.
30+
31+
Parameters
32+
----------
33+
url: str
34+
The URL of the file to stream from.
35+
dest: str
36+
The path to the destination file in the local file system. This path
37+
includes the file name.
38+
39+
Raises
40+
------
41+
InvalidHTTPResponseError
42+
If an error happens while streaming the file.
43+
OSError
44+
If the parent directory of ``dest`` doesn't exist.
45+
"""
46+
response = requests.get(url=url, stream=True, timeout=40)
47+
48+
if response.status_code != 200:
49+
raise InvalidHTTPResponseError(f"Cannot download java artifact file from {url}")
50+
51+
with open(dest, "wb") as fd:
52+
try:
53+
for chunk in response.iter_content(chunk_size=128, decode_unicode=False):
54+
fd.write(chunk)
55+
except requests.RequestException as error:
56+
response.close()
57+
raise InvalidHTTPResponseError(f"Error while streaming java artifact file from {url}") from error
58+
59+
60+
def join_remote_maven_repo_url(
61+
remote_maven_url: str,
62+
maven_repo_path: str,
63+
) -> str:
64+
"""Join the base remote maven URL with a maven repository path.
65+
66+
Parameters
67+
----------
68+
remote_maven_url: str
69+
The url to a remove maven layout repository.
70+
For example: https://repo1.maven.org/maven2
71+
maven_repo_path: str
72+
The maven repository path for a GAV coordinate or an artifact
73+
from the root of the remote maven layout repository.
74+
75+
Returns
76+
-------
77+
str
78+
The joined path.
79+
80+
Examples
81+
--------
82+
>>> remote_maven_repo = "https://repo1.maven.org/maven2"
83+
>>> artifact_path = "io/liftwizard/liftwizard-checkstyle/2.1.22/liftwizard-checkstyle-2.1.22.jar"
84+
>>> join_remote_maven_repo_url(remote_maven_repo, artifact_path)
85+
'https://repo1.maven.org/maven2/io/liftwizard/liftwizard-checkstyle/2.1.22/liftwizard-checkstyle-2.1.22.jar'
86+
>>> join_remote_maven_repo_url(remote_maven_repo, "io/liftwizard/liftwizard-checkstyle/2.1.22/")
87+
'https://repo1.maven.org/maven2/io/liftwizard/liftwizard-checkstyle/2.1.22/'
88+
>>> join_remote_maven_repo_url(f"{remote_maven_repo}/", artifact_path)
89+
'https://repo1.maven.org/maven2/io/liftwizard/liftwizard-checkstyle/2.1.22/liftwizard-checkstyle-2.1.22.jar'
90+
"""
91+
url_parse_result = urllib.parse.urlparse(remote_maven_url)
92+
new_path_component = os.path.join(
93+
url_parse_result.path,
94+
maven_repo_path,
95+
)
96+
return urllib.parse.urlunparse(
97+
urllib.parse.ParseResult(
98+
scheme=url_parse_result.scheme,
99+
netloc=url_parse_result.netloc,
100+
path=new_path_component,
101+
params="",
102+
query="",
103+
fragment="",
104+
)
105+
)
106+
107+
108+
def get_jdk_version_from_jar(artifact_path: str) -> str | None:
109+
"""Return the JDK version obtained from a Java artifact.
110+
111+
Parameters
112+
----------
113+
artifact_path: str
114+
The path to the artifact to extract the jdk version.
115+
116+
Returns
117+
-------
118+
str | None
119+
The version string extract from the artifact (as is) or None
120+
if there is an error, or if we couldn't find any jdk version.
121+
"""
122+
with zipfile.ZipFile(artifact_path, "r") as jar:
123+
manifest_path = "META-INF/MANIFEST.MF"
124+
with jar.open(manifest_path) as manifest_file:
125+
manifest_content = manifest_file.read().decode("utf-8")
126+
for line in manifest_content.splitlines():
127+
if "Build-Jdk" in line or "Build-Jdk-Spec" in line:
128+
_, _, version = line.rpartition(":")
129+
logger.debug(
130+
"Found JDK version %s from java artifact at %s",
131+
version.strip(),
132+
artifact_path,
133+
)
134+
return version.strip()
135+
136+
logger.debug("Cannot find any JDK version from java artifact at %s", artifact_path)
137+
return None
138+
139+
140+
def find_jdk_version_from_remote_maven_repo_standalone(
141+
group_id: str,
142+
artifact_id: str,
143+
version: str,
144+
asset_name: str,
145+
remote_maven_repo_url: str,
146+
) -> str | None:
147+
"""Return the jdk version string from an artifact matching a given GAV from a remote maven layout repository.
148+
149+
This function doesn't cache the downloaded artifact, and remove it after the function exits.
150+
We assume that the remote maven layout repository supports downloading a file through a HTTPS URL.
151+
152+
Parameters
153+
----------
154+
group_id: str
155+
The group ID part of the GAV coordinate.
156+
artifact_id: str
157+
The artifact ID part of the GAV coordinate.
158+
version: str
159+
The version part of the GAV coordinate.
160+
asset_name: str
161+
The name of artifact to download and extract the jdk version.
162+
ext: JavaArtifactExt
163+
The extension of the main artifact file.
164+
remote_maven_repo_url: str
165+
The URL to the remote maven layout repository.
166+
167+
Returns
168+
-------
169+
str | None
170+
The version string extract from the artifact (as is) or None
171+
ff there is an error, or if we couldn't find any jdk version.
172+
"""
173+
maven_repository_path = construct_maven_repository_path(
174+
group_id=group_id,
175+
artifact_id=artifact_id,
176+
version=version,
177+
asset_name=asset_name,
178+
)
179+
180+
artifact_url = join_remote_maven_repo_url(
181+
remote_maven_repo_url,
182+
maven_repository_path,
183+
)
184+
logger.debug(
185+
"Find JDK version from jar at %s, using temporary file.",
186+
artifact_url,
187+
)
188+
with tempfile.TemporaryDirectory() as temp_dir_name:
189+
local_artifact_path = os.path.join(temp_dir_name, asset_name)
190+
try:
191+
download_file(
192+
artifact_url,
193+
local_artifact_path,
194+
)
195+
except InvalidHTTPResponseError as error:
196+
logger.error("Failed why trying to download jar file. Error: %s", error)
197+
return None
198+
except OSError as os_error:
199+
logger.critical("Critical %s", os_error)
200+
return None
201+
202+
return get_jdk_version_from_jar(local_artifact_path)
203+
204+
205+
def find_jdk_version_from_remote_maven_repo_cache(
206+
group_id: str,
207+
artifact_id: str,
208+
version: str,
209+
asset_name: str,
210+
remote_maven_repo_url: str,
211+
local_cache_repo: str,
212+
) -> str | None:
213+
"""Return the jdk version string from an artifact matching a given GAV from a remote maven layout repository.
214+
215+
This function cache the downloaded artifact in a maven layout https://maven.apache.org/repository/layout.html
216+
undert ``local_cache_repo``.
217+
We assume that the remote maven layout repository supports downloading a file through a HTTPS URL.
218+
219+
Parameters
220+
----------
221+
group_id: str
222+
The group ID part of the GAV coordinate.
223+
artifact_id: str
224+
The artifact ID part of the GAV coordinate.
225+
version: str
226+
The version part of the GAV coordinate.
227+
asset_name: str
228+
The name of artifact to download and extract the jdk version.
229+
remote_maven_repo_url: str
230+
The URL to the remote maven layout repository.
231+
local_cache_repo: str
232+
The path to a local directory for caching the downloaded artifact used in JDK version
233+
extraction.
234+
235+
Returns
236+
-------
237+
str | None
238+
The version string extract from the artifact (as is) or None
239+
ff there is an error, or if we couldn't find any jdk version.
240+
"""
241+
maven_repository_path = construct_maven_repository_path(
242+
group_id=group_id,
243+
artifact_id=artifact_id,
244+
version=version,
245+
asset_name=asset_name,
246+
)
247+
248+
local_artifact_path = os.path.join(
249+
local_cache_repo,
250+
maven_repository_path,
251+
)
252+
if os.path.isfile(local_artifact_path):
253+
return get_jdk_version_from_jar(local_artifact_path)
254+
255+
gav_path = os.path.dirname(local_artifact_path)
256+
os.makedirs(
257+
gav_path,
258+
exist_ok=True,
259+
)
260+
261+
artifact_url = join_remote_maven_repo_url(
262+
remote_maven_repo_url,
263+
maven_repository_path,
264+
)
265+
logger.debug(
266+
"Find JDK version from jar at %s, using cache %s",
267+
artifact_url,
268+
local_artifact_path,
269+
)
270+
try:
271+
download_file(
272+
artifact_url,
273+
local_artifact_path,
274+
)
275+
except InvalidHTTPResponseError as error:
276+
logger.error("Failed why trying to download jar file. Error: %s", error)
277+
return None
278+
except OSError as os_error:
279+
logger.critical("Critical %s", os_error)
280+
return None
281+
282+
return get_jdk_version_from_jar(local_artifact_path)
283+
284+
285+
def find_jdk_version_from_central_maven_repo(
286+
group_id: str,
287+
artifact_id: str,
288+
version: str,
289+
use_cache: bool = True,
290+
) -> str | None:
291+
"""Return the jdk version string from an artifact matching a given GAV from Maven Central repository.
292+
293+
The artifacts will be downloaded from https://repo1.maven.org/maven2/ for JDK version extraction.
294+
295+
We now only support JAR files.
296+
297+
Parameters
298+
----------
299+
group_id: str
300+
The group ID part of the GAV coordinate.
301+
artifact_id: str
302+
The artifact ID part of the GAV coordinate.
303+
version: str
304+
The version part of the GAV coordinate.
305+
remote_maven_repo_url: str
306+
The URL to the remote maven layout repository.
307+
local_cache_repo: str
308+
The path to a local directory for caching the downloaded artifact used in JDK version
309+
extraction.
310+
311+
Returns
312+
-------
313+
str | None
314+
The version string extract from the artifact (as is) or None
315+
ff there is an error, or if we couldn't find any jdk version.
316+
"""
317+
central_repo_url = "https://repo1.maven.org/maven2/"
318+
local_cache_maven_repo = os.path.join(
319+
global_config.output_path,
320+
"jdk_finding_cache_maven_repo",
321+
)
322+
asset_name = f"{artifact_id}-{version}{JavaArtifactExt.JAR.value}"
323+
324+
if use_cache:
325+
return find_jdk_version_from_remote_maven_repo_cache(
326+
group_id=group_id,
327+
artifact_id=artifact_id,
328+
version=version,
329+
asset_name=asset_name,
330+
remote_maven_repo_url=central_repo_url,
331+
local_cache_repo=local_cache_maven_repo,
332+
)
333+
334+
return find_jdk_version_from_remote_maven_repo_standalone(
335+
group_id=group_id,
336+
artifact_id=artifact_id,
337+
version=version,
338+
asset_name=asset_name,
339+
remote_maven_repo_url=central_repo_url,
340+
)

0 commit comments

Comments
 (0)