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[PackageName] 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 	override Version[] getVersions(in PackageName name)
36 	{
37 		import std.algorithm.sorting : sort;
38 		auto md = getMetadata(name.main);
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 	override ubyte[] fetchPackage(in PackageName name,
51 		in VersionRange dep, bool pre_release)
52 	{
53 		import std.format : format;
54 		auto md = getMetadata(name.main);
55 		Json best = getBestPackage(md, name.main, dep, pre_release);
56 		if (best.type == Json.Type.null_)
57 			return null;
58 		auto vers = best["version"].get!string;
59 		auto url = m_mavenUrl ~ NativePath(
60 			"%s/%s/%s-%s.zip".format(name.main, vers, name.main, vers));
61 
62 		try {
63 			return retryDownload(url, 3, httpTimeout);
64 		}
65 		catch(HTTPStatusException e) {
66 			if (e.status == 404) throw e;
67 			else logDebug("Failed to download package %s from %s", name.main, url);
68 		}
69 		catch(Exception e) {
70 			logDebug("Failed to download package %s from %s", name.main, url);
71 		}
72 		throw new Exception("Failed to download package %s from %s".format(name.main, url));
73 	}
74 
75 	override Json fetchPackageRecipe(in PackageName name, in VersionRange dep,
76 		bool pre_release)
77 	{
78 		auto md = getMetadata(name);
79 		return getBestPackage(md, name, dep, pre_release);
80 	}
81 
82 	private Json getMetadata(in PackageName name)
83 	{
84 		import dub.internal.undead.xml;
85 
86 		auto now = Clock.currTime(UTC());
87 		if (auto pentry = name.main in m_metadataCache) {
88 			if (pentry.cacheTime + m_maxCacheTime > now)
89 				return pentry.data;
90 			m_metadataCache.remove(name.main);
91 		}
92 
93 		auto url = m_mavenUrl ~ NativePath(name.main.toString() ~ "/maven-metadata.xml");
94 
95 		logDebug("Downloading maven metadata for %s", name.main);
96 		string xmlData;
97 
98 		try
99 			xmlData = cast(string)retryDownload(url, 3, httpTimeout);
100 		catch(HTTPStatusException e) {
101 			if (e.status == 404) {
102 				logDebug("Maven metadata %s not found at %s (404): %s", name.main, description, e.msg);
103 				return Json(null);
104 			}
105 			else throw e;
106 		}
107 
108 		auto json = Json([
109 			"name": Json(name.main.toString()),
110 			"versions": Json.emptyArray
111 		]);
112 		auto xml = new DocumentParser(xmlData);
113 
114 		xml.onStartTag["versions"] = (ElementParser xml) {
115 			 xml.onEndTag["version"] = (in Element e) {
116 				json["versions"] ~= serializeToJson([
117 					"name": name.main.toString(),
118 					"version": e.text,
119 				]);
120 			 };
121 			 xml.parse();
122 		};
123 		xml.parse();
124 
125 		m_metadataCache[name.main] = CacheEntry(json, now);
126 		return json;
127 	}
128 
129 	SearchResult[] searchPackages(string query)
130 	{
131 		// Only exact search is supported
132 		// This enables retrieval of dub packages on dub run
133 		auto md = getMetadata(PackageName(query));
134 		if (md.type == Json.Type.null_)
135 			return null;
136 		auto json = getBestPackage(md, PackageName(query), VersionRange.Any, true);
137 		return [SearchResult(json["name"].opt!string, "", json["version"].opt!string)];
138 	}
139 }