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