[367475] Take git specificities into account when resolving uris.
[EMFCompare2.git] / plugins / org.eclipse.emf.compare.ide.ui / src / org / eclipse / emf / compare / ide / ui / logical / RevisionedURIConverter.java
blob5a31d441867e9d072e5c7bf1dd22cb4a2fd15d97
1 /*******************************************************************************
2 * Copyright (c) 2011, 2012 Obeo.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 * Obeo - initial API and implementation
10 *******************************************************************************/
11 package org.eclipse.emf.compare.ide.ui.logical;
13 import com.google.common.annotations.Beta;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.net.URISyntaxException;
18 import java.util.Map;
20 import org.eclipse.core.resources.IFile;
21 import org.eclipse.core.resources.IFileState;
22 import org.eclipse.core.resources.IProject;
23 import org.eclipse.core.resources.IResource;
24 import org.eclipse.core.resources.IStorage;
25 import org.eclipse.core.resources.IWorkspaceRoot;
26 import org.eclipse.core.resources.ResourcesPlugin;
27 import org.eclipse.core.runtime.CoreException;
28 import org.eclipse.core.runtime.IPath;
29 import org.eclipse.core.runtime.IStatus;
30 import org.eclipse.core.runtime.NullProgressMonitor;
31 import org.eclipse.core.runtime.Path;
32 import org.eclipse.core.runtime.Status;
33 import org.eclipse.emf.common.util.URI;
34 import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIPlugin;
35 import org.eclipse.emf.compare.ide.utils.StorageURIConverter;
36 import org.eclipse.emf.ecore.resource.URIConverter;
37 import org.eclipse.team.core.RepositoryProvider;
38 import org.eclipse.team.core.history.IFileHistory;
39 import org.eclipse.team.core.history.IFileHistoryProvider;
40 import org.eclipse.team.core.history.IFileRevision;
42 /**
43 * This {@link URIConverter} will be used in order to fetch remote contents instead of local contents when
44 * loading resources.
45 * <p>
46 * Let's suppose we are trying to compare "ecore.genmodel". This file depends on "ecore.ecore". However,
47 * "ecore.ecore" evolves more than its corresponding genmodel since all changes made in this file are not
48 * "breaking" for the genmodel. For example, version <i>1.19</i> of the genmodel was commited at the same time
49 * as version <i>1.17</i> of the ecore. If I compare my own copy of the genmodel with the "latest from HEAD"
50 * (version <i>1.19</i>), then this URI Converter will be used to resolve the proxy of that "remote" file with
51 * the corresponding "remote" version of the ecore file. In this case, it should be version <i>1.20</i>
52 * (latest non-breaking) of the ecore file, not the <i>1.17</i> that was commited along.
53 * </p>
54 * <p>
55 * To this end, when creating a revisioned URI converter, we give it a file revision that corresponds to what
56 * the user asked to compare, in the above example, the "latest from HEAD" of ecore.genmodel. We will fetch
57 * the revision that is "above" that one. In the case of the "latest", there is none. if we had been given
58 * version <i>1.18</i>, we would consider version <i>1.19</i>... And consider this "younger" revision to be
59 * the "upper bound" of the revisions we'll resolve for dependent fragments (in the example above,
60 * ecore.ecore).
61 * </p>
62 * <p>
63 * This resolution strategy means that we assume the users never broke their logical model by committing some
64 * fragments without the others (i.e. they never committed "ecore.ecore" without committing the
65 * "ecore.genmodel" if there were breaking changes made).
66 * </p>
68 * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
70 @Beta
71 public final class RevisionedURIConverter extends StorageURIConverter {
72 /** The revision of the base resource. This revision's timestamp will be used to resolve proxies. */
73 private IFileRevision baseRevision;
75 /** The actual Ifile from which was extracted {@link #baseRevision}. */
76 private IFile baseFile;
78 /**
79 * We need a "maximum" timestamp : see javadoc of the class.
80 * <p>
81 * <ul>
82 * <li><code>-2</code> is the unitialized value,</li>
83 * <li><code>-1</code> means we have no upper bound (use the latest revisions of the files),</li>
84 * <li>Any other value will be used to find the file revisions of our dependencies.</li>
85 * </p>
87 private long maxTimestamp = -2;
89 /**
90 * Instantiates our URI converter given its delegate.
92 * @param delegate
93 * Our delegate URI converter.
94 * @param baseRevision
95 * The base revision against which this URI converter should resolve URIs.
97 public RevisionedURIConverter(URIConverter delegate, IFileRevision baseRevision) {
98 super(delegate);
99 this.baseRevision = baseRevision;
100 this.baseFile = findFile(getFileURI(baseRevision).toString());
102 try {
103 getLoadedRevisions().add(baseRevision.getStorage(new NullProgressMonitor()));
104 } catch (CoreException e) {
105 // We cannot find the storage of our base revision?
108 final RepositoryProvider repositoryProvider = RepositoryProvider.getProvider(baseFile.getProject());
109 if (repositoryProvider == null) {
110 return;
113 final IFileHistoryProvider historyProvider = repositoryProvider.getFileHistoryProvider();
114 final IFileHistory history = historyProvider.getFileHistoryFor(baseFile, IFileHistoryProvider.NONE,
115 new NullProgressMonitor());
116 if (history != null) {
117 // We'll search for the revision "above" the given one in order to determine our upper bound
118 final IFileRevision[] revisions = history.getFileRevisions();
119 final String baseID = baseRevision.getContentIdentifier();
121 for (int i = 0; i < revisions.length && this.maxTimestamp == -2; i++) {
122 if (baseID.equals(revisions[i].getContentIdentifier())) {
123 if (i == 0) {
124 this.maxTimestamp = -1;
125 } else {
126 this.maxTimestamp = revisions[i - 1].getTimestamp();
134 * {@inheritDoc}
136 * @see org.eclipse.emf.compare.util.DelegatingURIConverter#createInputStream(org.eclipse.emf.common.util.URI,
137 * java.util.Map)
139 @SuppressWarnings("resource")
140 @Override
141 public InputStream createInputStream(URI uri, Map<?, ?> options) throws IOException {
142 InputStream stream = null;
144 final URI normalizedUri = normalize(uri);
145 // If this uri points to the plugins directory, load it directly
146 if (normalizedUri.isPlatformPlugin() || normalizedUri.toString().matches("(\\.\\./)+?plugins/.*")) { //$NON-NLS-1$
147 stream = super.createInputStream(normalizedUri, options);
148 } else {
149 // Otherwise, load it from the repository (resource might not yet (or no longer) exist locally)
150 final IResource targetFile;
151 if (normalizedUri.isPlatform()) {
152 IPath platformString = new Path(normalizedUri.trimFragment().toPlatformString(true));
153 IResource temp = ResourcesPlugin.getWorkspace().getRoot().getFile(platformString);
154 if (!temp.exists() && normalizedUri.isPlatformResource() && platformString.segmentCount() > 1) {
155 // We tend to get here with unresolvable URIs with git; as it tends to give URIs of the
156 // form
157 // platform:/resource/<repository name>/<workspace relative path> instead of the
158 // resolvable
159 // platform:/resource/<workspace relative path> . We'll try for this case
160 targetFile = ResourcesPlugin.getWorkspace().getRoot().getFile(
161 platformString.removeFirstSegments(1));
162 } else {
163 targetFile = temp;
165 } else {
167 * FIXME Deresolve the URI against the workspace root, if it cannot be done, delegate to
168 * super.createInputStream()
170 targetFile = ResourcesPlugin.getWorkspace().getRoot().getFile(
171 new Path(normalizedUri.trimFragment().toString()));
174 if (targetFile != null) {
175 stream = openRevisionStream(targetFile);
176 } else {
177 // FIXME The file URI couldn't be resolved in the workspace...
180 if (stream == null) {
181 return super.createInputStream(uri, options);
185 return stream;
189 * Opens an input stream on the contents of the first revision of the file designed by <em>targetURI</em>
190 * which timestamp is inferior or equal to that of {@link #baseRevision}. If no such revision exist, we'll
191 * use the closest to the {@link #baseRevision}'s timestamp we can find, hoping that it does correspond to
192 * the sought revision.
193 * <p>
194 * Take good note that the <em>targetFile</em> may not exist locally. This handle will only serve in order
195 * to retrieve its repository provider.
196 * </p>
198 * @param targetFile
199 * The resource we seek a revision of.
200 * @return The opened input stream. May be <code>null</code> if we failed to open it.
202 @SuppressWarnings("resource")
203 private InputStream openRevisionStream(IResource targetFile) {
204 IResource actualFile = targetFile;
205 if (!actualFile.exists()) {
206 // Can we relativize its path according to the baseRevision?
207 actualFile = findFile(actualFile.getFullPath().toString());
208 if (actualFile == null) {
209 actualFile = targetFile;
213 InputStream stream = null;
214 final RepositoryProvider repositoryProvider = RepositoryProvider.getProvider(actualFile.getProject());
216 if (repositoryProvider != null) {
217 final IFileHistoryProvider historyProvider = repositoryProvider.getFileHistoryProvider();
218 final IFileHistory history = historyProvider.getFileHistoryFor(actualFile,
219 IFileHistoryProvider.NONE, new NullProgressMonitor());
221 if (history != null) {
222 // This file exists on the repository.
223 IFileRevision soughtRevision = null;
224 final IFileRevision[] revisions = history.getFileRevisions();
225 for (int i = 0; i < revisions.length && soughtRevision == null; i++) {
226 final IFileRevision revision = revisions[i];
227 if (maxTimestamp < 0 || revision.getTimestamp() < maxTimestamp) {
228 soughtRevision = revision;
232 if (soughtRevision != null) {
233 try {
234 IStorage storage = soughtRevision.getStorage(new NullProgressMonitor());
235 getLoadedRevisions().add(storage);
236 stream = storage.getContents();
237 } catch (CoreException e) {
238 // failed to retrieve revision contents
239 logError(e);
245 if (stream == null) {
246 // Either this file is not connected to a repository, or we failed to retrieve a revision.
247 // Search through local history.
248 try {
249 IFileState soughtState = null;
250 final IFileState[] revisions = ((IFile)actualFile).getHistory(new NullProgressMonitor());
251 for (int i = 0; i < revisions.length && soughtState == null; i++) {
252 final IFileState revision = revisions[i];
253 if (maxTimestamp < 0 || revision.getModificationTime() < maxTimestamp) {
254 soughtState = revision;
258 if (soughtState != null) {
259 getLoadedRevisions().add(soughtState);
260 stream = soughtState.getContents();
261 } else {
262 getLoadedRevisions().add((IFile)actualFile);
263 stream = ((IFile)actualFile).getContents();
265 } catch (CoreException e) {
266 // failed to retrieve local contents
267 logError(e);
271 return stream;
275 * Tries and find an IFile corresponding to the given path.
277 * @param path
278 * The path for which we need an IFile.
279 * @return The IFile for the given path if we could find it, <code>null</code> otherwise.
281 private IFile findFile(String path) {
282 final java.net.URI baseURI = getFileURI(baseRevision);
283 java.net.URI targetURI = convertToURI(path);
284 java.net.URI relativizedURI = baseURI.relativize(targetURI);
285 final IPath relativizedPath;
286 if (relativizedURI.getPath().length() == 0) {
287 relativizedPath = new Path(targetURI.getPath());
288 } else {
289 relativizedPath = new Path(relativizedURI.getPath());
291 final String[] pathSegments = relativizedPath.segments();
294 * 'Repository' URIs cannot be found directly in the workspace : they are relative to the repository
295 * location. Let's try to find the IProject in which this file is located ...
297 final IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
298 IProject container = null;
300 int projectIndex = 0;
301 for (; projectIndex < pathSegments.length && (container == null || !container.exists()); projectIndex++) {
302 container = root.getProject(pathSegments[projectIndex]);
305 IFile file = null;
306 if (container != null && container.exists()) {
307 final IPath filePath = relativizedPath.removeFirstSegments(projectIndex);
308 file = container.getFile(filePath);
311 return file;
314 private java.net.URI getFileURI(IFileRevision revision) {
315 final java.net.URI baseURI = revision.getURI();
316 if (baseURI != null) {
317 return baseURI;
320 java.net.URI result = null;
321 try {
322 final IStorage storage = revision.getStorage(new NullProgressMonitor());
323 final String name = revision.getName();
325 IPath path = storage.getFullPath();
326 final String lastSegment = path.lastSegment();
327 if (!lastSegment.equals(name)) {
328 final int nameIndex = lastSegment.indexOf(name);
329 if (nameIndex != -1) {
330 path = path.removeLastSegments(1);
331 path = path.append(lastSegment.substring(0, nameIndex + name.length()));
335 result = new java.net.URI(path.toString());
336 } catch (CoreException e) {
337 logError(e);
338 } catch (URISyntaxException e) {
339 logError(e);
341 return result;
345 * Silently converts the given {@code path} into an {@link java.net.URI}.
347 * @param path
348 * The path for which we need a java URI.
349 * @return The converted URI if the path could be parsed as a valid URI, <code>null</code> otherwise.
351 private java.net.URI convertToURI(String path) {
352 try {
353 return new java.net.URI(path);
354 } catch (URISyntaxException e) {
355 return null;
360 * Logs the given exception as an error.
362 * @param e
363 * The exception we need to log.
365 private static void logError(Exception e) {
366 final IStatus status = new Status(IStatus.ERROR, EMFCompareIDEUIPlugin.PLUGIN_ID, e.getMessage(), e);
367 EMFCompareIDEUIPlugin.getDefault().getLog().log(status);