You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ManifestParser.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. /*
  2. * Copyright (C) 2015, Google Inc. and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.gitrepo;
  11. import java.io.FileInputStream;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.net.URI;
  15. import java.net.URISyntaxException;
  16. import java.text.MessageFormat;
  17. import java.util.ArrayList;
  18. import java.util.Collections;
  19. import java.util.HashMap;
  20. import java.util.HashSet;
  21. import java.util.Iterator;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.Set;
  25. import org.eclipse.jgit.annotations.NonNull;
  26. import org.eclipse.jgit.api.errors.GitAPIException;
  27. import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
  28. import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
  29. import org.eclipse.jgit.gitrepo.RepoProject.ReferenceFile;
  30. import org.eclipse.jgit.gitrepo.internal.RepoText;
  31. import org.eclipse.jgit.internal.JGitText;
  32. import org.eclipse.jgit.lib.Repository;
  33. import org.xml.sax.Attributes;
  34. import org.xml.sax.InputSource;
  35. import org.xml.sax.SAXException;
  36. import org.xml.sax.XMLReader;
  37. import org.xml.sax.helpers.DefaultHandler;
  38. import org.xml.sax.helpers.XMLReaderFactory;
  39. /**
  40. * Repo XML manifest parser.
  41. *
  42. * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
  43. * @since 4.0
  44. */
  45. public class ManifestParser extends DefaultHandler {
  46. private final String filename;
  47. private final URI baseUrl;
  48. private final String defaultBranch;
  49. private final Repository rootRepo;
  50. private final Map<String, Remote> remotes;
  51. private final Set<String> plusGroups;
  52. private final Set<String> minusGroups;
  53. private final List<RepoProject> projects;
  54. private final List<RepoProject> filteredProjects;
  55. private final IncludedFileReader includedReader;
  56. private String defaultRemote;
  57. private String defaultRevision;
  58. private int xmlInRead;
  59. private RepoProject currentProject;
  60. /**
  61. * A callback to read included xml files.
  62. */
  63. public interface IncludedFileReader {
  64. /**
  65. * Read a file from the same base dir of the manifest xml file.
  66. *
  67. * @param path
  68. * The relative path to the file to read
  69. * @return the {@code InputStream} of the file.
  70. * @throws GitAPIException
  71. * @throws IOException
  72. */
  73. public InputStream readIncludeFile(String path)
  74. throws GitAPIException, IOException;
  75. }
  76. /**
  77. * Constructor for ManifestParser
  78. *
  79. * @param includedReader
  80. * a
  81. * {@link org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader}
  82. * object.
  83. * @param filename
  84. * a {@link java.lang.String} object.
  85. * @param defaultBranch
  86. * a {@link java.lang.String} object.
  87. * @param baseUrl
  88. * a {@link java.lang.String} object.
  89. * @param groups
  90. * a {@link java.lang.String} object.
  91. * @param rootRepo
  92. * a {@link org.eclipse.jgit.lib.Repository} object.
  93. */
  94. public ManifestParser(IncludedFileReader includedReader, String filename,
  95. String defaultBranch, String baseUrl, String groups,
  96. Repository rootRepo) {
  97. this.includedReader = includedReader;
  98. this.filename = filename;
  99. this.defaultBranch = defaultBranch;
  100. this.rootRepo = rootRepo;
  101. this.baseUrl = normalizeEmptyPath(URI.create(baseUrl));
  102. plusGroups = new HashSet<>();
  103. minusGroups = new HashSet<>();
  104. if (groups == null || groups.length() == 0
  105. || groups.equals("default")) { //$NON-NLS-1$
  106. // default means "all,-notdefault"
  107. minusGroups.add("notdefault"); //$NON-NLS-1$
  108. } else {
  109. for (String group : groups.split(",")) { //$NON-NLS-1$
  110. if (group.startsWith("-")) //$NON-NLS-1$
  111. minusGroups.add(group.substring(1));
  112. else
  113. plusGroups.add(group);
  114. }
  115. }
  116. remotes = new HashMap<>();
  117. projects = new ArrayList<>();
  118. filteredProjects = new ArrayList<>();
  119. }
  120. /**
  121. * Read the xml file.
  122. *
  123. * @param inputStream
  124. * a {@link java.io.InputStream} object.
  125. * @throws java.io.IOException
  126. */
  127. public void read(InputStream inputStream) throws IOException {
  128. xmlInRead++;
  129. final XMLReader xr;
  130. try {
  131. xr = XMLReaderFactory.createXMLReader();
  132. } catch (SAXException e) {
  133. throw new IOException(JGitText.get().noXMLParserAvailable, e);
  134. }
  135. xr.setContentHandler(this);
  136. try {
  137. xr.parse(new InputSource(inputStream));
  138. } catch (SAXException e) {
  139. throw new IOException(RepoText.get().errorParsingManifestFile, e);
  140. }
  141. }
  142. /** {@inheritDoc} */
  143. @SuppressWarnings("nls")
  144. @Override
  145. public void startElement(
  146. String uri,
  147. String localName,
  148. String qName,
  149. Attributes attributes) throws SAXException {
  150. if (qName == null) {
  151. return;
  152. }
  153. switch (qName) {
  154. case "project":
  155. if (attributes.getValue("name") == null) {
  156. throw new SAXException(RepoText.get().invalidManifest);
  157. }
  158. currentProject = new RepoProject(attributes.getValue("name"),
  159. attributes.getValue("path"),
  160. attributes.getValue("revision"),
  161. attributes.getValue("remote"),
  162. attributes.getValue("groups"));
  163. currentProject
  164. .setRecommendShallow(attributes.getValue("clone-depth"));
  165. break;
  166. case "remote":
  167. String alias = attributes.getValue("alias");
  168. String fetch = attributes.getValue("fetch");
  169. String revision = attributes.getValue("revision");
  170. Remote remote = new Remote(fetch, revision);
  171. remotes.put(attributes.getValue("name"), remote);
  172. if (alias != null) {
  173. remotes.put(alias, remote);
  174. }
  175. break;
  176. case "default":
  177. defaultRemote = attributes.getValue("remote");
  178. defaultRevision = attributes.getValue("revision");
  179. break;
  180. case "copyfile":
  181. if (currentProject == null) {
  182. throw new SAXException(RepoText.get().invalidManifest);
  183. }
  184. currentProject.addCopyFile(new CopyFile(rootRepo,
  185. currentProject.getPath(), attributes.getValue("src"),
  186. attributes.getValue("dest")));
  187. break;
  188. case "linkfile":
  189. if (currentProject == null) {
  190. throw new SAXException(RepoText.get().invalidManifest);
  191. }
  192. currentProject.addLinkFile(new LinkFile(rootRepo,
  193. currentProject.getPath(), attributes.getValue("src"),
  194. attributes.getValue("dest")));
  195. break;
  196. case "include":
  197. String name = attributes.getValue("name");
  198. if (includedReader != null) {
  199. try (InputStream is = includedReader.readIncludeFile(name)) {
  200. if (is == null) {
  201. throw new SAXException(
  202. RepoText.get().errorIncludeNotImplemented);
  203. }
  204. read(is);
  205. } catch (Exception e) {
  206. throw new SAXException(MessageFormat
  207. .format(RepoText.get().errorIncludeFile, name), e);
  208. }
  209. } else if (filename != null) {
  210. int index = filename.lastIndexOf('/');
  211. String path = filename.substring(0, index + 1) + name;
  212. try (InputStream is = new FileInputStream(path)) {
  213. read(is);
  214. } catch (IOException e) {
  215. throw new SAXException(MessageFormat
  216. .format(RepoText.get().errorIncludeFile, path), e);
  217. }
  218. }
  219. break;
  220. case "remove-project": {
  221. String name2 = attributes.getValue("name");
  222. projects.removeIf((p) -> p.getName().equals(name2));
  223. break;
  224. }
  225. default:
  226. break;
  227. }
  228. }
  229. /** {@inheritDoc} */
  230. @Override
  231. public void endElement(
  232. String uri,
  233. String localName,
  234. String qName) throws SAXException {
  235. if ("project".equals(qName)) { //$NON-NLS-1$
  236. projects.add(currentProject);
  237. currentProject = null;
  238. }
  239. }
  240. /** {@inheritDoc} */
  241. @Override
  242. public void endDocument() throws SAXException {
  243. xmlInRead--;
  244. if (xmlInRead != 0)
  245. return;
  246. // Only do the following after we finished reading everything.
  247. Map<String, URI> remoteUrls = new HashMap<>();
  248. if (defaultRevision == null && defaultRemote != null) {
  249. Remote remote = remotes.get(defaultRemote);
  250. if (remote != null) {
  251. defaultRevision = remote.revision;
  252. }
  253. if (defaultRevision == null) {
  254. defaultRevision = defaultBranch;
  255. }
  256. }
  257. for (RepoProject proj : projects) {
  258. String remote = proj.getRemote();
  259. String revision = defaultRevision;
  260. if (remote == null) {
  261. if (defaultRemote == null) {
  262. if (filename != null) {
  263. throw new SAXException(MessageFormat.format(
  264. RepoText.get().errorNoDefaultFilename,
  265. filename));
  266. }
  267. throw new SAXException(RepoText.get().errorNoDefault);
  268. }
  269. remote = defaultRemote;
  270. } else {
  271. Remote r = remotes.get(remote);
  272. if (r != null && r.revision != null) {
  273. revision = r.revision;
  274. }
  275. }
  276. URI remoteUrl = remoteUrls.get(remote);
  277. if (remoteUrl == null) {
  278. String fetch = remotes.get(remote).fetch;
  279. if (fetch == null) {
  280. throw new SAXException(MessageFormat
  281. .format(RepoText.get().errorNoFetch, remote));
  282. }
  283. remoteUrl = normalizeEmptyPath(baseUrl.resolve(fetch));
  284. remoteUrls.put(remote, remoteUrl);
  285. }
  286. proj.setUrl(remoteUrl.resolve(proj.getName()).toString())
  287. .setDefaultRevision(revision);
  288. }
  289. filteredProjects.addAll(projects);
  290. removeNotInGroup();
  291. removeOverlaps();
  292. }
  293. static URI normalizeEmptyPath(URI u) {
  294. // URI.create("scheme://host").resolve("a/b") => "scheme://hosta/b"
  295. // That seems like bug https://bugs.openjdk.java.net/browse/JDK-4666701.
  296. // We workaround this by special casing the empty path case.
  297. if (u.getHost() != null && !u.getHost().isEmpty() &&
  298. (u.getPath() == null || u.getPath().isEmpty())) {
  299. try {
  300. return new URI(u.getScheme(),
  301. u.getUserInfo(), u.getHost(), u.getPort(),
  302. "/", u.getQuery(), u.getFragment()); //$NON-NLS-1$
  303. } catch (URISyntaxException x) {
  304. throw new IllegalArgumentException(x.getMessage(), x);
  305. }
  306. }
  307. return u;
  308. }
  309. /**
  310. * Getter for projects.
  311. *
  312. * @return projects list reference, never null
  313. */
  314. public List<RepoProject> getProjects() {
  315. return projects;
  316. }
  317. /**
  318. * Getter for filterdProjects.
  319. *
  320. * @return filtered projects list reference, never null
  321. */
  322. @NonNull
  323. public List<RepoProject> getFilteredProjects() {
  324. return filteredProjects;
  325. }
  326. /** Remove projects that are not in our desired groups. */
  327. void removeNotInGroup() {
  328. Iterator<RepoProject> iter = filteredProjects.iterator();
  329. while (iter.hasNext())
  330. if (!inGroups(iter.next()))
  331. iter.remove();
  332. }
  333. /** Remove projects that sits in a subdirectory of any other project. */
  334. void removeOverlaps() {
  335. Collections.sort(filteredProjects);
  336. Iterator<RepoProject> iter = filteredProjects.iterator();
  337. if (!iter.hasNext())
  338. return;
  339. RepoProject last = iter.next();
  340. while (iter.hasNext()) {
  341. RepoProject p = iter.next();
  342. if (last.isAncestorOf(p))
  343. iter.remove();
  344. else
  345. last = p;
  346. }
  347. removeNestedCopyAndLinkfiles();
  348. }
  349. private void removeNestedCopyAndLinkfiles() {
  350. for (RepoProject proj : filteredProjects) {
  351. List<CopyFile> copyfiles = new ArrayList<>(proj.getCopyFiles());
  352. proj.clearCopyFiles();
  353. for (CopyFile copyfile : copyfiles) {
  354. if (!isNestedReferencefile(copyfile)) {
  355. proj.addCopyFile(copyfile);
  356. }
  357. }
  358. List<LinkFile> linkfiles = new ArrayList<>(proj.getLinkFiles());
  359. proj.clearLinkFiles();
  360. for (LinkFile linkfile : linkfiles) {
  361. if (!isNestedReferencefile(linkfile)) {
  362. proj.addLinkFile(linkfile);
  363. }
  364. }
  365. }
  366. }
  367. boolean inGroups(RepoProject proj) {
  368. for (String group : minusGroups) {
  369. if (proj.inGroup(group)) {
  370. // minus groups have highest priority.
  371. return false;
  372. }
  373. }
  374. if (plusGroups.isEmpty() || plusGroups.contains("all")) { //$NON-NLS-1$
  375. // empty plus groups means "all"
  376. return true;
  377. }
  378. for (String group : plusGroups) {
  379. if (proj.inGroup(group))
  380. return true;
  381. }
  382. return false;
  383. }
  384. private boolean isNestedReferencefile(ReferenceFile referencefile) {
  385. if (referencefile.dest.indexOf('/') == -1) {
  386. // If the referencefile is at root level then it won't be nested.
  387. return false;
  388. }
  389. for (RepoProject proj : filteredProjects) {
  390. if (proj.getPath().compareTo(referencefile.dest) > 0) {
  391. // Early return as remaining projects can't be ancestor of this
  392. // referencefile config (filteredProjects is sorted).
  393. return false;
  394. }
  395. if (proj.isAncestorOf(referencefile.dest)) {
  396. return true;
  397. }
  398. }
  399. return false;
  400. }
  401. private static class Remote {
  402. final String fetch;
  403. final String revision;
  404. Remote(String fetch, String revision) {
  405. this.fetch = fetch;
  406. this.revision = revision;
  407. }
  408. }
  409. }