1 package bldr;
  2 
  3 import static bldr.Bldr.assertExists;
  4 import static bldr.Bldr.curl;
  5 import static java.io.IO.print;
  6 import static java.io.IO.println;
  7 
  8 import java.net.MalformedURLException;
  9 import java.net.URI;
 10 import java.net.URISyntaxException;
 11 import java.net.URL;
 12 import java.net.URLEncoder;
 13 import java.nio.charset.StandardCharsets;
 14 import java.util.ArrayList;
 15 import java.util.Comparator;
 16 import java.util.HashMap;
 17 import java.util.HashSet;
 18 import java.util.List;
 19 import java.util.Map;
 20 import java.util.Optional;
 21 import java.util.Set;
 22 import java.util.regex.Matcher;
 23 import java.util.regex.Pattern;
 24 import java.util.stream.Stream;
 25 import org.w3c.dom.Element;
 26 import org.w3c.dom.Node;
 27 
 28 public class MavenStyleRepository {
 29   private final String repoBase = "https://repo1.maven.org/maven2/";
 30   private final String searchBase = "https://search.maven.org/solrsearch/";
 31   public Bldr.BuildDir dir;
 32 
 33   Bldr.JarFile jarFile(Id id) {
 34     return dir.jarFile(id.artifactAndVersion() + ".jar");
 35   }
 36 
 37   Bldr.XMLFile pomFile(Id id) {
 38     return dir.xmlFile(id.artifactAndVersion() + ".pom");
 39   }
 40 
 41   public enum Scope {
 42     TEST,
 43     COMPILE,
 44     PROVIDED,
 45     RUNTIME,
 46     SYSTEM;
 47 
 48     static Scope of(String name) {
 49       return switch (name.toLowerCase()) {
 50         case "test" -> TEST;
 51         case "compile" -> COMPILE;
 52         case "provided" -> PROVIDED;
 53         case "runtime" -> RUNTIME;
 54         case "system" -> SYSTEM;
 55         default -> COMPILE;
 56       };
 57     }
 58   }
 59 
 60   public record GroupAndArtifactId(GroupId groupId, ArtifactId artifactId) {
 61 
 62     public static GroupAndArtifactId of(String groupAndArtifactId) {
 63       int idx = groupAndArtifactId.indexOf('/');
 64       return of(groupAndArtifactId.substring(0, idx), groupAndArtifactId.substring(idx + 1));
 65     }
 66 
 67     public static GroupAndArtifactId of(GroupId groupId, ArtifactId artifactId) {
 68       return new GroupAndArtifactId(groupId, artifactId);
 69     }
 70 
 71     public static GroupAndArtifactId of(String groupId, String artifactId) {
 72       return of(GroupId.of(groupId), ArtifactId.of(artifactId));
 73     }
 74 
 75     String location() {
 76       return groupId().string().replace('.', '/') + "/" + artifactId().string();
 77     }
 78 
 79     @Override
 80     public String toString() {
 81       return groupId() + "/" + artifactId();
 82     }
 83   }
 84 
 85   public sealed interface Id permits DependencyId, MetaDataId {
 86     MavenStyleRepository mavenStyleRepository();
 87 
 88     GroupAndArtifactId groupAndArtifactId();
 89 
 90     VersionId versionId();
 91 
 92     default String artifactAndVersion() {
 93       return groupAndArtifactId().artifactId().string() + '-' + versionId();
 94     }
 95 
 96     default String location() {
 97       return mavenStyleRepository().repoBase + groupAndArtifactId().location() + "/" + versionId();
 98     }
 99 
100     default URL url(String suffix) {
101       try {
102         return new URI(location() + "/" + artifactAndVersion() + "." + suffix).toURL();
103       } catch (MalformedURLException | URISyntaxException e) {
104         throw new RuntimeException(e);
105       }
106     }
107   }
108 
109   public record DependencyId(
110       MavenStyleRepository mavenStyleRepository,
111       GroupAndArtifactId groupAndArtifactId,
112       VersionId versionId,
113       Scope scope,
114       boolean required)
115       implements Id {
116     @Override
117     public String toString() {
118       return groupAndArtifactId().toString()
119           + "/"
120           + versionId()
121           + ":"
122           + scope.toString()
123           + ":"
124           + (required ? "Required" : "Optiona");
125     }
126   }
127 
128   public record Pom(MetaDataId metaDataId, XMLNode xmlNode) {
129     Bldr.JarFile getJar() {
130       var jarFile = metaDataId.mavenStyleRepository().jarFile(metaDataId); // ;
131       metaDataId.mavenStyleRepository.queryAndCache(metaDataId.jarURL(), jarFile);
132       return jarFile;
133     }
134 
135     String description() {
136       return xmlNode().xpathQueryString("/project/description/text()");
137     }
138 
139     Stream<DependencyId> dependencies() {
140       return xmlNode()
141           .nodes(xmlNode.xpath("/project/dependencies/dependency"))
142           .map(node -> new XMLNode((Element) node))
143           .map(
144               dependency ->
145                   new DependencyId(
146                       metaDataId().mavenStyleRepository(),
147                       GroupAndArtifactId.of(
148                           GroupId.of(dependency.xpathQueryString("groupId/text()")),
149                           ArtifactId.of(dependency.xpathQueryString("artifactId/text()"))),
150                       VersionId.of(dependency.xpathQueryString("version/text()")),
151                       Scope.of(dependency.xpathQueryString("scope/text()")),
152                       !Boolean.parseBoolean(dependency.xpathQueryString("optional/text()"))));
153     }
154 
155     Stream<DependencyId> requiredDependencies() {
156       return dependencies().filter(DependencyId::required);
157     }
158   }
159 
160   public Optional<Pom> pom(Id id) {
161     return switch (id) {
162       case MetaDataId metaDataId -> {
163         if (metaDataId.versionId() == VersionId.UNSPECIFIED) {
164           // println("what to do when the version is unspecified");
165           yield Optional.empty();
166         }
167         try {
168           yield Optional.of(
169               new Pom(
170                   metaDataId,
171                   queryAndCache(
172                       metaDataId.pomURL(), metaDataId.mavenStyleRepository.pomFile(metaDataId))));
173         } catch (Throwable e) {
174           throw new RuntimeException(e);
175         }
176       }
177       case DependencyId dependencyId -> {
178         if (metaData(
179                     id.groupAndArtifactId().groupId().string(),
180                     id.groupAndArtifactId().artifactId().string())
181                 instanceof Optional<MetaData> optionalMetaData
182             && optionalMetaData.isPresent()) {
183           if (optionalMetaData
184                       .get()
185                       .metaDataIds()
186                       .filter(metaDataId -> metaDataId.versionId().equals(id.versionId()))
187                       .findFirst()
188                   instanceof Optional<MetaDataId> metaId
189               && metaId.isPresent()) {
190             yield pom(metaId.get());
191           } else {
192             yield Optional.empty();
193           }
194         } else {
195           yield Optional.empty();
196         }
197       }
198       default -> throw new IllegalStateException("Unexpected value: " + id);
199     };
200   }
201 
202   public Optional<Pom> pom(GroupAndArtifactId groupAndArtifactId) {
203     var metaData = metaData(groupAndArtifactId).orElseThrow();
204     var metaDataId = metaData.latestMetaDataId().orElseThrow();
205     return pom(metaDataId);
206   }
207 
208   record IdVersions(GroupAndArtifactId groupAndArtifactId, Set<Id> versions) {
209     static IdVersions of(GroupAndArtifactId groupAndArtifactId) {
210       return new IdVersions(groupAndArtifactId, new HashSet<>());
211     }
212   }
213 
214 
215 
216   public static class Dag implements Bldr.ClassPathEntryProvider {
217     private final MavenStyleRepository repo;
218     private final List<GroupAndArtifactId> rootGroupAndArtifactIds;
219     Map<GroupAndArtifactId, IdVersions> nodes = new HashMap<>();
220     Map<IdVersions, List<IdVersions>> edges = new HashMap<>();
221 
222     Dag add(Id from, Id to) {
223       var fromNode =
224               nodes.computeIfAbsent(
225                       from.groupAndArtifactId(), _ -> IdVersions.of(from.groupAndArtifactId()));
226       fromNode.versions().add(from);
227       var toNode =
228               nodes.computeIfAbsent(
229                       to.groupAndArtifactId(), _ -> IdVersions.of(to.groupAndArtifactId()));
230       toNode.versions().add(to);
231       edges.computeIfAbsent(fromNode, k -> new ArrayList<>()).add(toNode);
232       return this;
233     }
234 
235     void removeUNSPECIFIED() {
236       nodes
237               .values()
238               .forEach(
239                       idversions -> {
240                         if (idversions.versions().size() > 1) {
241                           List<Id> versions = new ArrayList<>(idversions.versions());
242                           idversions.versions().clear();
243                           idversions
244                                   .versions()
245                                   .addAll(
246                                           versions.stream()
247                                                   .filter(v -> !v.versionId().equals(VersionId.UNSPECIFIED))
248                                                   .toList());
249                           println(idversions);
250                         }
251                         if (idversions.versions().size() > 1) {
252                           throw new IllegalStateException("more than one version");
253                         }
254                       });
255     }
256 
257     Dag(MavenStyleRepository repo, List<GroupAndArtifactId> rootGroupAndArtifactIds) {
258       this.repo = repo;
259       this.rootGroupAndArtifactIds = rootGroupAndArtifactIds;
260 
261       Set<Id> unresolved = new HashSet<>();
262       rootGroupAndArtifactIds.forEach(
263               rootGroupAndArtifactId -> {
264                 var metaData = repo.metaData(rootGroupAndArtifactId).orElseThrow();
265                 var metaDataId = metaData.latestMetaDataId().orElseThrow();
266                 var optionalPom = repo.pom(rootGroupAndArtifactId);
267 
268                 if (optionalPom.isPresent() && optionalPom.get() instanceof Pom pom) {
269                   pom.requiredDependencies()
270                           .filter(dependencyId -> !dependencyId.scope.equals(Scope.TEST))
271                           .forEach(
272                                   dependencyId -> {
273                                     add(metaDataId, dependencyId);
274                                     unresolved.add(dependencyId);
275                                   });
276                 }
277               });
278 
279       while (!unresolved.isEmpty()) {
280         var resolveSet = new HashSet<>(unresolved);
281         unresolved.clear();
282         resolveSet.forEach(
283                 id -> {
284                   if (repo.pom(id) instanceof Optional<Pom> p && p.isPresent()) {
285                     p.get()
286                             .requiredDependencies()
287                             .filter(dependencyId -> !dependencyId.scope.equals(Scope.TEST))
288                             .forEach(
289                                     dependencyId -> {
290                                       unresolved.add(dependencyId);
291                                       add(id, dependencyId);
292                                     });
293                     // }else{
294                     // throw new IllegalArgumentException("unresolved pom " + id);
295                   }
296                 });
297       }
298       removeUNSPECIFIED();
299     }
300 
301     @Override public List<Bldr.ClassPathEntry> classPathEntries(){
302       return classPath().classPathEntries();
303     }
304 
305     Bldr.ClassPath classPath() {
306 
307       Bldr.ClassPath jars = Bldr.ClassPath.of();
308       nodes
309               .keySet()
310               .forEach(
311                       id -> {
312                         // println("looking for pom for "+id);
313                         Optional<Pom> optionalPom = repo.pom(id);
314                         if (optionalPom.isPresent() && optionalPom.get() instanceof Pom pom) {
315                           jars.add(pom.getJar());
316                         } else {
317                           throw new RuntimeException("No pom for " + id + " needed by " + id);
318                         }
319                       });
320       return jars;
321     }
322   }
323 
324   public Dag dag(String... rootGroupAndArtifactIds) {
325     return dag(Stream.of(rootGroupAndArtifactIds).map(GroupAndArtifactId::of).toList());
326   }
327 
328   public Dag dag(GroupAndArtifactId... rootGroupAndArtifactIds) {
329     return dag(List.of(rootGroupAndArtifactIds));
330   }
331 
332   public Dag dag(List<GroupAndArtifactId> rootGroupAndArtifactIds) {
333     var dag = new Dag(this, rootGroupAndArtifactIds);
334     return dag;
335   }
336 
337 
338   public record VersionId(Integer maj, Integer min, Integer point, String classifier)
339       implements Comparable<VersionId> {
340     static Integer integerOrNull(String s) {
341       return (s == null || s.isEmpty()) ? null : Integer.parseInt(s);
342     }
343 
344     public static Pattern pattern = Pattern.compile("^(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(.*))?)?$");
345     static VersionId UNSPECIFIED = new VersionId(null, null, null, null);
346 
347     static VersionId of(String version) {
348       Matcher matcher = pattern.matcher(version);
349       if (matcher.matches()) {
350         return new VersionId(
351             integerOrNull(matcher.group(1)),
352             integerOrNull(matcher.group(2)),
353             integerOrNull(matcher.group(3)),
354             matcher.group(4));
355       } else {
356         return UNSPECIFIED;
357       }
358     }
359 
360     int cmp(Integer v1, Integer v2) {
361       if (v1 == null && v2 == null) {
362         return 0;
363       }
364       if (v1 == null) {
365         return -v2;
366       } else if (v2 == null) {
367         return v1;
368       } else {
369         return v1 - v2;
370       }
371     }
372 
373     @Override
374     public int compareTo(VersionId o) {
375       if (cmp(maj(), o.maj()) == 0) {
376         if (cmp(min(), o.min()) == 0) {
377           if (cmp(point(), o.point()) == 0) {
378             return classifier().compareTo(o.classifier());
379           } else {
380             return cmp(point(), o.point());
381           }
382         } else {
383           return cmp(min(), o.min());
384         }
385       } else {
386         return cmp(maj(), o.maj());
387       }
388     }
389 
390     @Override
391     public String toString() {
392       StringBuilder sb = new StringBuilder();
393       if (maj() != null) {
394         sb.append(maj());
395         if (min() != null) {
396           sb.append(".").append(min());
397           if (point() != null) {
398             sb.append(".").append(point());
399             if (classifier() != null) {
400               sb.append(classifier());
401             }
402           }
403         }
404       } else {
405         sb.append("UNSPECIFIED");
406       }
407       return sb.toString();
408     }
409   }
410 
411   public record GroupId(String string) {
412     public static GroupId of(String s) {
413       return new GroupId(s);
414     }
415 
416     @Override
417     public String toString() {
418       return string;
419     }
420   }
421 
422   public record ArtifactId(String string) {
423     static ArtifactId of(String string) {
424       return new ArtifactId(string);
425     }
426 
427     @Override
428     public String toString() {
429       return string;
430     }
431   }
432 
433   public record MetaDataId(
434       MavenStyleRepository mavenStyleRepository,
435       GroupAndArtifactId groupAndArtifactId,
436       VersionId versionId,
437       Set<String> downloadables,
438       Set<String> tags)
439       implements Id {
440 
441     public URL pomURL() {
442       return url("pom");
443     }
444 
445     public URL jarURL() {
446       return url("jar");
447     }
448 
449     public XMLNode getPom() {
450       if (downloadables.contains(".pom")) {
451         return mavenStyleRepository.queryAndCache(
452             url("pom"), mavenStyleRepository.dir.xmlFile(artifactAndVersion() + ".pom"));
453       } else {
454         throw new IllegalStateException("no pom");
455       }
456     }
457 
458     @Override
459     public String toString() {
460       return groupAndArtifactId().toString() + "." + versionId();
461     }
462   }
463 
464   public MavenStyleRepository(Bldr.BuildDir dir) {
465     this.dir = dir.create();
466   }
467 
468   Bldr.JarFile queryAndCache(URL query, Bldr.JarFile jarFile) {
469     try {
470       if (!jarFile.exists()) {
471         print("Querying and caching " + jarFile.fileName());
472         println(" downloading " + query);
473         curl(query, jarFile.path());
474       } else {
475         // println("Using cached " + jarFile.fileName());
476 
477       }
478     } catch (Throwable e) {
479       throw new RuntimeException(e);
480     }
481     return jarFile;
482   }
483 
484   XMLNode queryAndCache(URL query, Bldr.XMLFile xmlFile) {
485     XMLNode xmlNode = null;
486     try {
487       if (!xmlFile.exists()) {
488         print("Querying and caching " + xmlFile.fileName());
489         println(" downloading " + query);
490         xmlNode = new XMLNode(query);
491         xmlNode.write(xmlFile.path().toFile());
492       } else {
493         // println("Using cached " + xmlFile.fileName());
494         xmlNode = new XMLNode(xmlFile.path());
495       }
496     } catch (Throwable e) {
497       throw new RuntimeException(e);
498     }
499     return xmlNode;
500   }
501 
502   public record MetaData(
503       MavenStyleRepository mavenStyleRepository,
504       GroupAndArtifactId groupAndArtifactId,
505       XMLNode xmlNode) {
506 
507     public Stream<MetaDataId> metaDataIds() {
508       return xmlNode
509           .xmlNodes(xmlNode.xpath("/response/result/doc"))
510           .map(
511               xmln ->
512                   new MetaDataId(
513                       this.mavenStyleRepository,
514                       GroupAndArtifactId.of(
515                           GroupId.of(xmln.xpathQueryString("str[@name='g']/text()")),
516                           ArtifactId.of(xmln.xpathQueryString("str[@name='a']/text()"))),
517                       VersionId.of(xmln.xpathQueryString("str[@name='v']/text()")),
518                       new HashSet<>(
519                           xmln.nodes(xmln.xpath("arr[@name='ec']/str/text()"))
520                               .map(Node::getNodeValue)
521                               .toList()),
522                       new HashSet<>(
523                           xmln.nodes(xmln.xpath("arr[@name='tags']/str/text()"))
524                               .map(Node::getNodeValue)
525                               .toList())));
526     }
527 
528     public Stream<MetaDataId> sortedMetaDataIds() {
529       return metaDataIds().sorted(Comparator.comparing(MetaDataId::versionId));
530     }
531 
532     public Optional<MetaDataId> latestMetaDataId() {
533       return metaDataIds().max(Comparator.comparing(MetaDataId::versionId));
534     }
535 
536     public Optional<MetaDataId> getMetaDataId(VersionId versionId) {
537       return metaDataIds().filter(id -> versionId.compareTo(id.versionId()) == 0).findFirst();
538     }
539   }
540 
541   public Optional<MetaData> metaData(String groupId, String artifactId) {
542     return metaData(GroupAndArtifactId.of(groupId, artifactId));
543   }
544 
545   public Optional<MetaData> metaData(GroupAndArtifactId groupAndArtifactId) {
546     try {
547       var query = "g:" + groupAndArtifactId.groupId() + " AND a:" + groupAndArtifactId.artifactId();
548       URL rowQueryUrl =
549           new URI(
550                   searchBase
551                       + "select?q="
552                       + URLEncoder.encode(query, StandardCharsets.UTF_8)
553                       + "&core=gav&wt=xml&rows=0")
554               .toURL();
555       var rowQueryResponse = new XMLNode(rowQueryUrl);
556       var numFound = rowQueryResponse.xpathQueryString("/response/result/@numFound");
557 
558       URL url =
559           new URI(
560                   searchBase
561                       + "select?q="
562                       + URLEncoder.encode(query, StandardCharsets.UTF_8)
563                       + "&core=gav&wt=xml&rows="
564                       + numFound)
565               .toURL();
566       try {
567         // println(url);
568         var xmlNode =
569             queryAndCache(url, dir.xmlFile(groupAndArtifactId.artifactId() + ".meta.xml"));
570         // var numFound2 = xmlNode.xpathQueryString("/response/result/@numFound");
571         // var start = xmlNode.xpathQueryString("/response/result/@start");
572         // var rows =
573         // xmlNode.xpathQueryString("/response/lst[@name='responseHeader']/lst[@name='params']/str[@name='rows']/text()");
574         // println("numFound = "+numFound+" rows ="+rows+ " start ="+start);
575         if (numFound.isEmpty() || numFound.equals("0")) {
576           return Optional.empty();
577         } else {
578           return Optional.of(new MetaData(this, groupAndArtifactId, xmlNode));
579         }
580       } catch (Throwable e) {
581         throw new RuntimeException(e);
582       }
583     } catch (Throwable e) {
584       throw new RuntimeException(e);
585     }
586   }
587 
588   public static void main(String[] args) {
589 
590     var hatDir = assertExists(Bldr.Dir.of("/Users/grfrost/github/babylon-grfrost-fork/hat"));
591 
592     var repo = new MavenStyleRepository(hatDir.buildDir("repo"));
593     var testngMeta = repo.metaData("org.testng", "testng");
594     if (testngMeta.isPresent()) {
595       testngMeta
596           .get()
597           .sortedMetaDataIds()
598           .forEach(
599               metaDataId -> {
600                 var pom = metaDataId.getPom();
601                 //   println(pom.toString());
602               });
603     }
604     var junitMeta = repo.metaData("org.junit.platform", "junit-platform-console-standalone");
605     if (junitMeta.isPresent()) {
606       var latest = junitMeta.get().latestMetaDataId();
607       if (latest.isPresent()) {
608         var id = latest.get();
609         var pom = id.getPom();
610         // println(pom.toString());
611       }
612     }
613   }
614 }