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 14KB

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