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