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