maven_toolbox/
lib.rs

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        // TODO other fields
27        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    // RepositoryError,
152}
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        // a little helper
224        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        // merge in the dependencies from the parent POM
276        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                // TODO add protection against infinite recursion
307                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        // we're looking only for POMs here
333        let project_id = project_id.with_packaging("pom");
334
335        // check the cache first
336        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        // grab the remote POM
342        let url = self.create_url(&project_id)?;
343
344        log::debug!("fetching {}...", url);
345        let text = url_fetcher.fetch(&url)?;
346
347        // parse the POM - it will be our "root" project
348        // TODO handle multiple "roots"
349        let mut project = pom_parser.parse(text)?;
350
351        // make sure the packaging type is set to "pom"
352        let mut project_id = project.artifact_fqn.with_packaging("pom");
353
354        // TODO consider moving this to build_effective_pom
355        // update the parent and fill-in the project's missing properties using the parent's GAV
356        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            // normalize dependency GAVs
362            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            // save the updated FQN
369            project.parent = project.parent.map(|mut p| {
370                p.artifact_fqn = parent_fqn;
371                p
372            });
373        }
374
375        // save the updated FQN
376        project.artifact_fqn = project_id.clone();
377
378        // we're going to save all parsed projects into a HashMap
379        // as a "cache"
380        log::trace!("caching {}", project_id);
381        self.project_cache.insert(project_id, project.clone());
382
383        Ok(project)
384    }
385}