1use std::collections::HashMap;
2
3#[cfg(feature = "default-impl")]
4pub mod default_impl;
5
6#[derive(Default, Debug, Clone, Eq, PartialEq, Hash)]
7pub struct ArtifactFqn {
8 pub group_id: Option<String>,
9 pub artifact_id: Option<String>,
10 pub version: Option<String>,
11 pub packaging: Option<String>,
12}
13
14impl ArtifactFqn {
15 pub fn pom(group_id: &str, artifact_id: &str, version: &str) -> Self {
16 ArtifactFqn {
17 group_id: Some(group_id.to_owned()),
18 artifact_id: Some(artifact_id.to_owned()),
19 version: Some(version.to_owned()),
20 packaging: Some("pom".to_owned()),
21 ..Default::default()
22 }
23 }
24
25 pub fn interpolate(&self, properties: &HashMap<String, String>) -> Self {
26 ArtifactFqn {
28 version: self
29 .version
30 .clone()
31 .filter(|v| v.contains("${"))
32 .map(|mut s| {
33 if let Some(start) = s.find("${") {
34 if let Some(end) = s[start..].find("}") {
35 let expr = s[start + 2..end].to_owned();
36 if let Some(v) = properties.get(&expr) {
37 s.replace_range(start..end + 1, v);
38 }
39 }
40 }
41 s
42 })
43 .or_else(|| self.version.clone()),
44 ..self.clone()
45 }
46 }
47
48 pub fn with_packaging(&self, packaging: &str) -> Self {
49 ArtifactFqn {
50 packaging: Some(packaging.to_owned()),
51 ..self.clone()
52 }
53 }
54
55 pub fn same_ga(&self, other: &Self) -> bool {
56 self.group_id == other.group_id && self.artifact_id == other.artifact_id
57 }
58
59 pub fn normalize(self, parent: &Self, default_packaging: &str) -> Self {
60 ArtifactFqn {
61 group_id: self.group_id.or_else(|| parent.group_id.clone()),
62 artifact_id: self.artifact_id.or_else(|| parent.artifact_id.clone()),
63 version: self.version.or_else(|| parent.version.clone()),
64 packaging: self
65 .packaging
66 .or_else(|| Some(default_packaging.to_owned())),
67 }
68 }
69}
70
71impl std::fmt::Display for ArtifactFqn {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 let def = "?".to_owned();
74 write!(
75 f,
76 "{}:{}:{}:{}",
77 self.group_id.as_ref().unwrap_or(&def),
78 self.artifact_id.as_ref().unwrap_or(&def),
79 self.version.as_ref().unwrap_or(&def),
80 self.packaging.as_ref().unwrap_or(&def)
81 )
82 }
83}
84
85#[derive(Default, Debug, Clone)]
86pub struct Dependency {
87 pub artifact_fqn: ArtifactFqn,
88 pub scope: Option<String>,
89}
90
91impl Dependency {
92 pub fn get_key(&self) -> DependencyKey {
93 DependencyKey {
94 group_id: self.artifact_fqn.group_id.clone(),
95 artifact_id: self.artifact_fqn.artifact_id.clone(),
96 }
97 }
98
99 pub fn normalize(self, parent_id: &ArtifactFqn, default_packaging: &str) -> Self {
100 Dependency {
101 artifact_fqn: self.artifact_fqn.normalize(parent_id, default_packaging),
102 scope: self.scope.or_else(|| Some("compile".to_owned())),
103 }
104 }
105}
106
107#[derive(Debug, Clone)]
108pub struct Parent {
109 pub artifact_fqn: ArtifactFqn,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Hash)]
113pub struct DependencyKey {
114 pub group_id: Option<String>,
115 pub artifact_id: Option<String>,
116}
117
118impl std::fmt::Display for DependencyKey {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 let def = "?".to_owned();
121 write!(
122 f,
123 "{}:{}",
124 self.group_id.as_ref().unwrap_or(&def),
125 self.artifact_id.as_ref().unwrap_or(&def)
126 )
127 }
128}
129
130#[derive(Debug, Clone)]
131pub struct DependencyManagement {
132 pub dependencies: HashMap<DependencyKey, Dependency>,
133}
134
135#[derive(Debug, Clone)]
136pub struct Project {
137 pub parent: Option<Parent>,
138 pub artifact_fqn: ArtifactFqn,
139 pub dependency_management: Option<DependencyManagement>,
140 pub dependencies: HashMap<DependencyKey, Dependency>,
141 pub properties: HashMap<String, String>,
142}
143
144pub struct Repository {
145 pub base_url: String,
146}
147
148#[derive(Debug)]
149pub enum ErrorKind {
150 ClientError,
151 }
153
154#[derive(Debug)]
155pub struct ResolverError {
156 pub kind: ErrorKind,
157 pub msg: String,
158}
159
160impl ResolverError {
161 pub fn missing_parameter<D: std::fmt::Display>(fqn: &ArtifactFqn, field_name: &D) -> Self {
162 ResolverError {
163 kind: ErrorKind::ClientError,
164 msg: format!("'{}' is missing from {}", field_name, fqn),
165 }
166 }
167
168 pub fn invalid_data(details: &str) -> Self {
169 ResolverError {
170 kind: ErrorKind::ClientError,
171 msg: format!("Invalid input data: {}", details),
172 }
173 }
174
175 pub fn cant_resolve(artifact_id: &ArtifactFqn, cause: &str) -> Self {
176 ResolverError {
177 kind: ErrorKind::ClientError,
178 msg: format!("Can't resolve {:?}: {}", artifact_id, cause),
179 }
180 }
181}
182
183pub trait UrlFetcher {
184 fn fetch(&self, url: &str) -> Result<String, ResolverError>;
185}
186
187pub trait PomParser {
188 fn parse(&self, input: String) -> Result<Project, ResolverError>;
189}
190
191pub struct Resolver {
192 pub repository: Repository,
193 pub project_cache: HashMap<ArtifactFqn, Project>,
194}
195
196impl Default for Resolver {
197 fn default() -> Self {
198 Resolver {
199 repository: Repository {
200 base_url: "https://repo.maven.apache.org/maven2".into(),
201 },
202 project_cache: HashMap::new(),
203 }
204 }
205}
206
207fn normalize_gavs(
208 dependencies: HashMap<DependencyKey, Dependency>,
209 parent_fqn: &ArtifactFqn,
210 default_packaging: &str,
211) -> HashMap<DependencyKey, Dependency> {
212 dependencies
213 .into_iter()
214 .map(|(_, dep)| {
215 let dep = dep.normalize(parent_fqn, default_packaging);
216 (dep.get_key(), dep)
217 })
218 .collect()
219}
220
221impl Resolver {
222 pub fn create_url(&self, id: &ArtifactFqn) -> Result<String, ResolverError> {
223 fn require<'a, F, D>(
225 id: &'a ArtifactFqn,
226 f: F,
227 field_name: &D,
228 ) -> Result<&'a String, ResolverError>
229 where
230 F: Fn(&ArtifactFqn) -> Option<&String>,
231 D: std::fmt::Display,
232 {
233 f(id).ok_or_else(|| ResolverError::missing_parameter(id, field_name))
234 }
235
236 let group_id = require(id, |id| id.group_id.as_ref(), &"groupId")?;
237 let artifact_id = require(id, |id| id.artifact_id.as_ref(), &"artifactId")?;
238 let version = require(id, |id| id.version.as_ref(), &"version")?;
239 let packaging = require(id, |id| id.packaging.as_ref(), &"packaging")?;
240
241 Ok(format!(
242 "{}/{}/{}/{}/{}-{}.{}",
243 self.repository.base_url,
244 group_id.replace(".", "/"),
245 artifact_id,
246 version,
247 artifact_id,
248 version,
249 packaging
250 ))
251 }
252
253 pub fn build_effective_pom<UF, P>(
254 &mut self,
255 project_id: &ArtifactFqn,
256 url_fetcher: &UF,
257 pom_parser: &P,
258 ) -> Result<Project, ResolverError>
259 where
260 UF: UrlFetcher,
261 P: PomParser,
262 {
263 log::debug!("building an effective pom for {}", project_id);
264
265 let project_id = &project_id.with_packaging("pom");
266
267 let mut project = self.fetch_project(project_id, url_fetcher, pom_parser)?;
268
269 if let Some(version) = &project_id.version {
270 project
271 .properties
272 .insert("project.version".to_owned(), version.clone());
273 }
274
275 if let Some(parent) = &project.parent {
277 let parent_project =
278 self.build_effective_pom(&parent.artifact_fqn, url_fetcher, pom_parser)?;
279
280 log::trace!("got a parent POM: {}", parent_project.artifact_fqn);
281
282 let extra_deps = parent_project
283 .dependencies
284 .into_iter()
285 .filter(|(dep_key, _)| !project.dependencies.contains_key(dep_key))
286 .collect::<HashMap<_, _>>();
287
288 project.dependencies.extend(extra_deps);
289 }
290
291 if let Some(mut project_dm) = project.dependency_management.clone() {
292 for (_, dep) in &mut project_dm.dependencies {
293 dep.artifact_fqn = dep.artifact_fqn.interpolate(&project.properties);
294 }
295
296 let boms: Vec<Dependency> = project_dm
297 .dependencies
298 .iter()
299 .filter(|(_, dep)| dep.scope.as_deref() == Some("import"))
300 .map(|(_, dep)| dep.clone())
301 .collect();
302
303 for bom in boms {
304 log::trace!("got a BOM artifact: {}", bom.artifact_fqn);
305
306 let bom_project =
308 self.build_effective_pom(&bom.artifact_fqn, url_fetcher, pom_parser)?;
309
310 if let Some(DependencyManagement {
311 dependencies: bom_deps,
312 }) = bom_project.dependency_management
313 {
314 project_dm.dependencies.extend(bom_deps);
315 }
316 }
317 };
318
319 Ok(project)
320 }
321
322 pub fn fetch_project<UF, P>(
323 &mut self,
324 project_id: &ArtifactFqn,
325 url_fetcher: &UF,
326 pom_parser: &P,
327 ) -> Result<Project, ResolverError>
328 where
329 UF: UrlFetcher,
330 P: PomParser,
331 {
332 let project_id = project_id.with_packaging("pom");
334
335 if let Some(cached_project) = self.project_cache.get(&project_id) {
337 log::debug!("returning from cache {}...", project_id);
338 return Ok(cached_project.clone());
339 }
340
341 let url = self.create_url(&project_id)?;
343
344 log::debug!("fetching {}...", url);
345 let text = url_fetcher.fetch(&url)?;
346
347 let mut project = pom_parser.parse(text)?;
350
351 let mut project_id = project.artifact_fqn.with_packaging("pom");
353
354 if let Some(parent) = &project.parent {
357 let parent_fqn = parent.artifact_fqn.with_packaging("pom");
358
359 project_id = project_id.normalize(&parent_fqn, "pom");
360
361 project.dependencies = normalize_gavs(project.dependencies, &parent_fqn, "jar");
363 project.dependency_management = project.dependency_management.map(|mut dm| {
364 dm.dependencies = normalize_gavs(dm.dependencies, &parent_fqn, "jar");
365 dm
366 });
367
368 project.parent = project.parent.map(|mut p| {
370 p.artifact_fqn = parent_fqn;
371 p
372 });
373 }
374
375 project.artifact_fqn = project_id.clone();
377
378 log::trace!("caching {}", project_id);
381 self.project_cache.insert(project_id, project.clone());
382
383 Ok(project)
384 }
385}