1 module dub.packagesuppliers.maven;
2 
3 import dub.packagesuppliers.packagesupplier;
4 
5 /**
6 	Maven repository based package supplier.
7 
8 	This package supplier connects to a maven repository
9 	to search for available packages.
10 */
11 class MavenRegistryPackageSupplier : PackageSupplier {
12 	import dub.internal.utils : retryDownload, HTTPStatusException;
13 	import dub.internal.vibecompat.data.json : serializeToJson;
14 	import dub.internal.vibecompat.inet.path : InetPath;
15 	import dub.internal.vibecompat.inet.url : URL;
16 	import dub.internal.logging;
17 
18 	import std.datetime : Clock, Duration, hours, SysTime, UTC;
19 
20 	private {
21 		enum httpTimeout = 16;
22 		URL m_mavenUrl;
23 		struct CacheEntry { Json data; SysTime cacheTime; }
24 		CacheEntry[PackageName] m_metadataCache;
25 		Duration m_maxCacheTime;
26 	}
27 
28 	this(URL mavenUrl)
29 	{
30 		m_mavenUrl = mavenUrl;
31 		m_maxCacheTime = 24.hours();
32 	}
33 
34 	override @property string description() { return "maven repository at "~m_mavenUrl.toString(); }
35 
36 	override Version[] getVersions(in PackageName name)
37 	{
38 		import std.algorithm.sorting : sort;
39 		auto md = getMetadata(name.main);
40 		if (md.type == Json.Type.null_)
41 			return null;
42 		Version[] ret;
43 		foreach (json; md["versions"]) {
44 			auto cur = Version(json["version"].get!string);
45 			ret ~= cur;
46 		}
47 		ret.sort();
48 		return ret;
49 	}
50 
51 	override ubyte[] fetchPackage(in PackageName name,
52 		in VersionRange dep, bool pre_release)
53 	{
54 		import std.format : format;
55 		auto md = getMetadata(name.main);
56 		Json best = getBestPackage(md, name.main, dep, pre_release);
57 		if (best.type == Json.Type.null_)
58 			return null;
59 		auto vers = best["version"].get!string;
60 		auto url = m_mavenUrl ~ InetPath(
61 			"%s/%s/%s-%s.zip".format(name.main, vers, name.main, vers));
62 
63 		try {
64 			return retryDownload(url, 3, httpTimeout);
65 		}
66 		catch(HTTPStatusException e) {
67 			if (e.status == 404) throw e;
68 			else logDebug("Failed to download package %s from %s", name.main, url);
69 		}
70 		catch(Exception e) {
71 			logDebug("Failed to download package %s from %s", name.main, url);
72 		}
73 		throw new Exception("Failed to download package %s from %s".format(name.main, url));
74 	}
75 
76 	override Json fetchPackageRecipe(in PackageName name, in VersionRange dep,
77 		bool pre_release)
78 	{
79 		auto md = getMetadata(name);
80 		return getBestPackage(md, name, dep, pre_release);
81 	}
82 
83 	private Json getMetadata(in PackageName name)
84 	{
85 		import dub.internal.undead.xml;
86 
87 		auto now = Clock.currTime(UTC());
88 		if (auto pentry = name.main in m_metadataCache) {
89 			if (pentry.cacheTime + m_maxCacheTime > now)
90 				return pentry.data;
91 			m_metadataCache.remove(name.main);
92 		}
93 
94 		auto url = m_mavenUrl ~ InetPath(name.main.toString() ~ "/maven-metadata.xml");
95 
96 		logDebug("Downloading maven metadata for %s", name.main);
97 		string xmlData;
98 
99 		try
100 			xmlData = cast(string)retryDownload(url, 3, httpTimeout);
101 		catch(HTTPStatusException e) {
102 			if (e.status == 404) {
103 				logDebug("Maven metadata %s not found at %s (404): %s", name.main, description, e.msg);
104 				return Json(null);
105 			}
106 			else throw e;
107 		}
108 
109 		auto json = Json([
110 			"name": Json(name.main.toString()),
111 			"versions": Json.emptyArray
112 		]);
113 		auto xml = new DocumentParser(xmlData);
114 
115 		xml.onStartTag["versions"] = (ElementParser xml) {
116 			 xml.onEndTag["version"] = (in Element e) {
117 				json["versions"] ~= serializeToJson([
118 					"name": name.main.toString(),
119 					"version": e.text,
120 				]);
121 			 };
122 			 xml.parse();
123 		};
124 		xml.parse();
125 
126 		m_metadataCache[name.main] = CacheEntry(json, now);
127 		return json;
128 	}
129 
130 	SearchResult[] searchPackages(string query)
131 	{
132 		// Only exact search is supported
133 		// This enables retrieval of dub packages on dub run
134 		auto md = getMetadata(PackageName(query));
135 		if (md.type == Json.Type.null_)
136 			return null;
137 		auto json = getBestPackage(md, PackageName(query), VersionRange.Any, true);
138 		return [SearchResult(json["name"].opt!string, "", json["version"].opt!string)];
139 	}
140 }