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