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.api.errors.GitAPIException;
61  import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
62  import org.eclipse.jgit.gitrepo.internal.RepoText;
63  import org.eclipse.jgit.internal.JGitText;
64  import org.eclipse.jgit.lib.Repository;
65  import org.xml.sax.Attributes;
66  import org.xml.sax.InputSource;
67  import org.xml.sax.SAXException;
68  import org.xml.sax.XMLReader;
69  import org.xml.sax.helpers.DefaultHandler;
70  import org.xml.sax.helpers.XMLReaderFactory;
71  
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 String baseUrl;
81  	private final String defaultBranch;
82  	private final Repository rootRepo;
83  	private final Map<String, String> 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  
90  	private String defaultRemote;
91  	private String defaultRevision;
92  	private int xmlInRead;
93  	private RepoProject currentProject;
94  
95  	/**
96  	 * A callback to read included xml files.
97  	 */
98  	public interface IncludedFileReader {
99  		/**
100 		 * Read a file from the same base dir of the manifest xml file.
101 		 *
102 		 * @param path
103 		 *            The relative path to the file to read
104 		 * @return the {@code InputStream} of the file.
105 		 * @throws GitAPIException
106 		 * @throws IOException
107 		 */
108 		public InputStream readIncludeFile(String path)
109 				throws GitAPIException, IOException;
110 	}
111 
112 	/**
113 	 * @param includedReader
114 	 * @param filename
115 	 * @param defaultBranch
116 	 * @param baseUrl
117 	 * @param groups
118 	 * @param rootRepo
119 	 */
120 	public ManifestParser(IncludedFileReader includedReader, String filename,
121 			String defaultBranch, String baseUrl, String groups,
122 			Repository rootRepo) {
123 		this.includedReader = includedReader;
124 		this.filename = filename;
125 		this.defaultBranch = defaultBranch;
126 		this.rootRepo = rootRepo;
127 
128 		// Strip trailing /s to match repo behavior.
129 		int lastIndex = baseUrl.length() - 1;
130 		while (lastIndex >= 0 && baseUrl.charAt(lastIndex) == '/')
131 			lastIndex--;
132 		this.baseUrl = baseUrl.substring(0, lastIndex + 1);
133 
134 		plusGroups = new HashSet<String>();
135 		minusGroups = new HashSet<String>();
136 		if (groups == null || groups.length() == 0
137 				|| groups.equals("default")) { //$NON-NLS-1$
138 			// default means "all,-notdefault"
139 			minusGroups.add("notdefault"); //$NON-NLS-1$
140 		} else {
141 			for (String group : groups.split(",")) { //$NON-NLS-1$
142 				if (group.startsWith("-")) //$NON-NLS-1$
143 					minusGroups.add(group.substring(1));
144 				else
145 					plusGroups.add(group);
146 			}
147 		}
148 
149 		remotes = new HashMap<String, String>();
150 		projects = new ArrayList<RepoProject>();
151 		filteredProjects = new ArrayList<RepoProject>();
152 	}
153 
154 	/**
155 	 * Read the xml file.
156 	 *
157 	 * @param inputStream
158 	 * @throws 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 			IOException error = new IOException(
173 						RepoText.get().errorParsingManifestFile);
174 			error.initCause(e);
175 			throw error;
176 		}
177 	}
178 
179 	@Override
180 	public void startElement(
181 			String uri,
182 			String localName,
183 			String qName,
184 			Attributes attributes) throws SAXException {
185 		if ("project".equals(qName)) { //$NON-NLS-1$
186 			if (attributes.getValue("name") == null) { //$NON-NLS-1$
187 				throw new SAXException(RepoText.get().invalidManifest);
188 			}
189 			currentProject = new RepoProject(
190 					attributes.getValue("name"), //$NON-NLS-1$
191 					attributes.getValue("path"), //$NON-NLS-1$
192 					attributes.getValue("revision"), //$NON-NLS-1$
193 					attributes.getValue("remote"), //$NON-NLS-1$
194 					attributes.getValue("groups")); //$NON-NLS-1$
195 		} else if ("remote".equals(qName)) { //$NON-NLS-1$
196 			String alias = attributes.getValue("alias"); //$NON-NLS-1$
197 			String fetch = attributes.getValue("fetch"); //$NON-NLS-1$
198 			remotes.put(attributes.getValue("name"), fetch); //$NON-NLS-1$
199 			if (alias != null)
200 				remotes.put(alias, fetch);
201 		} else if ("default".equals(qName)) { //$NON-NLS-1$
202 			defaultRemote = attributes.getValue("remote"); //$NON-NLS-1$
203 			defaultRevision = attributes.getValue("revision"); //$NON-NLS-1$
204 			if (defaultRevision == null)
205 				defaultRevision = defaultBranch;
206 		} else if ("copyfile".equals(qName)) { //$NON-NLS-1$
207 			if (currentProject == null)
208 				throw new SAXException(RepoText.get().invalidManifest);
209 			currentProject.addCopyFile(new CopyFile(
210 						rootRepo,
211 						currentProject.getPath(),
212 						attributes.getValue("src"), //$NON-NLS-1$
213 						attributes.getValue("dest"))); //$NON-NLS-1$
214 		} else if ("include".equals(qName)) { //$NON-NLS-1$
215 			String name = attributes.getValue("name"); //$NON-NLS-1$
216 			InputStream is = null;
217 			if (includedReader != null) {
218 				try {
219 					is = includedReader.readIncludeFile(name);
220 				} catch (Exception e) {
221 					throw new SAXException(MessageFormat.format(
222 							RepoText.get().errorIncludeFile, name), e);
223 				}
224 			} else if (filename != null) {
225 				int index = filename.lastIndexOf('/');
226 				String path = filename.substring(0, index + 1) + name;
227 				try {
228 					is = new FileInputStream(path);
229 				} catch (IOException e) {
230 					throw new SAXException(MessageFormat.format(
231 							RepoText.get().errorIncludeFile, path), e);
232 				}
233 			}
234 			if (is == null) {
235 				throw new SAXException(
236 						RepoText.get().errorIncludeNotImplemented);
237 			}
238 			try {
239 				read(is);
240 			} catch (IOException e) {
241 				throw new SAXException(e);
242 			}
243 		}
244 	}
245 
246 	@Override
247 	public void endElement(
248 			String uri,
249 			String localName,
250 			String qName) throws SAXException {
251 		if ("project".equals(qName)) { //$NON-NLS-1$
252 			projects.add(currentProject);
253 			currentProject = null;
254 		}
255 	}
256 
257 	@Override
258 	public void endDocument() throws SAXException {
259 		xmlInRead--;
260 		if (xmlInRead != 0)
261 			return;
262 
263 		// Only do the following after we finished reading everything.
264 		Map<String, String> remoteUrls = new HashMap<String, String>();
265 		URI baseUri;
266 		try {
267 			baseUri = new URI(baseUrl);
268 		} catch (URISyntaxException e) {
269 			throw new SAXException(e);
270 		}
271 		for (RepoProject proj : projects) {
272 			String remote = proj.getRemote();
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 					else
280 						throw new SAXException(
281 								RepoText.get().errorNoDefault);
282 				}
283 				remote = defaultRemote;
284 			}
285 			String remoteUrl = remoteUrls.get(remote);
286 			if (remoteUrl == null) {
287 				remoteUrl = baseUri.resolve(remotes.get(remote)).toString();
288 				if (!remoteUrl.endsWith("/")) //$NON-NLS-1$
289 					remoteUrl = remoteUrl + "/"; //$NON-NLS-1$
290 				remoteUrls.put(remote, remoteUrl);
291 			}
292 			proj.setUrl(remoteUrl + proj.getName())
293 					.setDefaultRevision(defaultRevision);
294 		}
295 
296 		filteredProjects.addAll(projects);
297 		removeNotInGroup();
298 		removeOverlaps();
299 	}
300 
301 	/**
302 	 * Getter for projects.
303 	 *
304 	 * @return projects list reference, never null
305 	 */
306 	public List<RepoProject> getProjects() {
307 		return projects;
308 	}
309 
310 	/**
311 	 * Getter for filterdProjects.
312 	 *
313 	 * @return filtered projects list reference, never null
314 	 */
315 	public List<RepoProject> getFilteredProjects() {
316 		return filteredProjects;
317 	}
318 
319 	/** Remove projects that are not in our desired groups. */
320 	void removeNotInGroup() {
321 		Iterator<RepoProject> iter = filteredProjects.iterator();
322 		while (iter.hasNext())
323 			if (!inGroups(iter.next()))
324 				iter.remove();
325 	}
326 
327 	/** Remove projects that sits in a subdirectory of any other project. */
328 	void removeOverlaps() {
329 		Collections.sort(filteredProjects);
330 		Iterator<RepoProject> iter = filteredProjects.iterator();
331 		if (!iter.hasNext())
332 			return;
333 		RepoProject last = iter.next();
334 		while (iter.hasNext()) {
335 			RepoProject p = iter.next();
336 			if (last.isAncestorOf(p))
337 				iter.remove();
338 			else
339 				last = p;
340 		}
341 	}
342 
343 	boolean inGroups(RepoProject proj) {
344 		for (String group : minusGroups) {
345 			if (proj.inGroup(group)) {
346 				// minus groups have highest priority.
347 				return false;
348 			}
349 		}
350 		if (plusGroups.isEmpty() || plusGroups.contains("all")) { //$NON-NLS-1$
351 			// empty plus groups means "all"
352 			return true;
353 		}
354 		for (String group : plusGroups) {
355 			if (proj.inGroup(group))
356 				return true;
357 		}
358 		return false;
359 	}
360 }