Use Set instead of array to keep track of change listeners
[egit/torarne.git] / org.spearce.egit.core / src / org / spearce / egit / core / project / GitProjectData.java
blobb12a85fb346edb28f543b089e05668b4f790a6e0
1 /*******************************************************************************
2 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
3 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4 * Copyright (C) 2008, Google Inc.
6 * All rights reserved. This program and the accompanying materials
7 * are made available under the terms of the Eclipse Public License v1.0
8 * See LICENSE for the full license text, also available.
9 *******************************************************************************/
10 package org.spearce.egit.core.project;
12 import java.io.File;
13 import java.io.FileInputStream;
14 import java.io.FileNotFoundException;
15 import java.io.FileOutputStream;
16 import java.io.IOException;
17 import java.lang.ref.Reference;
18 import java.lang.ref.WeakReference;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.Iterator;
24 import java.util.Map;
25 import java.util.Properties;
26 import java.util.Set;
28 import org.eclipse.core.resources.IContainer;
29 import org.eclipse.core.resources.IProject;
30 import org.eclipse.core.resources.IResource;
31 import org.eclipse.core.resources.IResourceChangeEvent;
32 import org.eclipse.core.resources.IResourceChangeListener;
33 import org.eclipse.core.resources.ResourcesPlugin;
34 import org.eclipse.core.runtime.CoreException;
35 import org.eclipse.core.runtime.Preferences;
36 import org.eclipse.core.runtime.QualifiedName;
37 import org.eclipse.osgi.util.NLS;
38 import org.eclipse.team.core.RepositoryProvider;
39 import org.spearce.egit.core.Activator;
40 import org.spearce.egit.core.CoreText;
41 import org.spearce.egit.core.GitCorePreferences;
42 import org.spearce.egit.core.GitProvider;
43 import org.spearce.jgit.lib.Repository;
44 import org.spearce.jgit.lib.WindowCache;
46 /**
47 * This class keeps information about how a project is mapped to
48 * a Git repository.
50 public class GitProjectData {
51 private static final Map<IProject, GitProjectData> projectDataCache = new HashMap<IProject, GitProjectData>();
53 private static final Map<File, WeakReference> repositoryCache = new HashMap<File, WeakReference>();
55 private static Set<RepositoryChangeListener> repositoryChangeListeners = new HashSet<RepositoryChangeListener>();
57 @SuppressWarnings("synthetic-access")
58 private static final IResourceChangeListener rcl = new RCL();
60 private static class RCL implements IResourceChangeListener {
61 @SuppressWarnings("synthetic-access")
62 public void resourceChanged(final IResourceChangeEvent event) {
63 switch (event.getType()) {
64 case IResourceChangeEvent.PRE_CLOSE:
65 uncache((IProject) event.getResource());
66 break;
67 case IResourceChangeEvent.PRE_DELETE:
68 delete((IProject) event.getResource());
69 break;
70 default:
71 break;
76 private static QualifiedName MAPPING_KEY = new QualifiedName(
77 GitProjectData.class.getName(), "RepositoryMapping");
79 /**
80 * Start listening for resource changes.
82 * @param includeChange true to listen to content changes
84 public static void attachToWorkspace(final boolean includeChange) {
85 trace("attachToWorkspace - addResourceChangeListener");
86 ResourcesPlugin.getWorkspace().addResourceChangeListener(
87 rcl,
88 (includeChange ? IResourceChangeEvent.POST_CHANGE : 0)
89 | IResourceChangeEvent.PRE_CLOSE
90 | IResourceChangeEvent.PRE_DELETE);
93 /**
94 * Stop listening to resource changes
96 public static void detachFromWorkspace() {
97 trace("detachFromWorkspace - removeResourceChangeListener");
98 ResourcesPlugin.getWorkspace().removeResourceChangeListener(rcl);
102 * Register a new listener for repository modification events.
103 * <p>
104 * This is a no-op if <code>objectThatCares</code> has already been
105 * registered.
106 * </p>
108 * @param objectThatCares
109 * the new listener to register. Must not be null.
111 public static synchronized void addRepositoryChangeListener(
112 final RepositoryChangeListener objectThatCares) {
113 if (objectThatCares == null)
114 throw new NullPointerException();
115 repositoryChangeListeners.add(objectThatCares);
119 * Remove a registered {@link RepositoryChangeListener}
121 * @param objectThatCares
122 * The listener to remove
124 public static synchronized void removeRepositoryChangeListener(
125 final RepositoryChangeListener objectThatCares) {
126 repositoryChangeListeners.remove(objectThatCares);
130 * Notify registered {@link RepositoryChangeListener}s of a change.
132 * @param which
133 * the repository which has had changes occur within it.
135 static void fireRepositoryChanged(final RepositoryMapping which) {
136 for (RepositoryChangeListener listener : getRepositoryChangeListeners())
137 listener.repositoryChanged(which);
141 * Get a copy of the current set of repository change listeners
142 * <p>
143 * The array has no references, so is safe for iteration and modification
145 * @return a copy of the current repository change listeners
147 private static synchronized RepositoryChangeListener[] getRepositoryChangeListeners() {
148 return repositoryChangeListeners
149 .toArray(new RepositoryChangeListener[repositoryChangeListeners
150 .size()]);
154 * @param p
155 * @return {@link GitProjectData} for the specified project
157 public synchronized static GitProjectData get(final IProject p) {
158 try {
159 GitProjectData d = lookup(p);
160 if (d == null
161 && RepositoryProvider.getProvider(p) instanceof GitProvider) {
162 d = new GitProjectData(p).load();
163 cache(p, d);
165 return d;
166 } catch (IOException err) {
167 Activator.logError(CoreText.GitProjectData_missing, err);
168 return null;
173 * Drop the Eclipse project from our association of projects/repositories
175 * @param p Eclipse project
177 public static void delete(final IProject p) {
178 trace("delete(" + p.getName() + ")");
179 GitProjectData d = lookup(p);
180 if (d == null) {
181 try {
182 d = new GitProjectData(p).load();
183 } catch (IOException ioe) {
184 d = new GitProjectData(p);
187 d.delete();
190 static void trace(final String m) {
191 Activator.trace("(GitProjectData) " + m);
194 private synchronized static void cache(final IProject p,
195 final GitProjectData d) {
196 projectDataCache.put(p, d);
199 private synchronized static void uncache(final IProject p) {
200 if (projectDataCache.remove(p) != null) {
201 trace("uncacheDataFor(" + p.getName() + ")");
205 private synchronized static GitProjectData lookup(final IProject p) {
206 return projectDataCache.get(p);
209 private synchronized static Repository lookupRepository(final File gitDir)
210 throws IOException {
211 final Iterator i = repositoryCache.entrySet().iterator();
212 while (i.hasNext()) {
213 final Map.Entry e = (Map.Entry) i.next();
214 if (((Reference) e.getValue()).get() == null) {
215 i.remove();
219 final Reference r = repositoryCache.get(gitDir);
220 Repository d = r != null ? (Repository) r.get() : null;
221 if (d == null) {
222 d = new Repository(gitDir);
223 repositoryCache.put(gitDir, new WeakReference<Repository>(d));
225 return d;
229 * Update the settings for the global window cache of the workspace.
231 public static void reconfigureWindowCache() {
232 Preferences p = Activator.getDefault().getPluginPreferences();
233 int wLimit = p.getInt(GitCorePreferences.core_packedGitLimit);
234 int wSize = p.getInt(GitCorePreferences.core_packedGitWindowSize);
235 boolean mmap = p.getBoolean(GitCorePreferences.core_packedGitMMAP);
236 int dbLimit = p.getInt(GitCorePreferences.core_deltaBaseCacheLimit);
237 WindowCache.reconfigure(wLimit, wSize, mmap, dbLimit);
240 private final IProject project;
242 private final Collection<RepositoryMapping> mappings = new ArrayList<RepositoryMapping>();
244 private final Set<IResource> protectedResources = new HashSet<IResource>();
247 * Construct a {@link GitProjectData} for the mapping
248 * of a project.
250 * @param p Eclipse project
252 public GitProjectData(final IProject p) {
253 project = p;
257 * @return the Eclipse project mapped through this resource.
259 public IProject getProject() {
260 return project;
264 * TODO: is this right?
266 * @param newMappings
268 public void setRepositoryMappings(final Collection<RepositoryMapping> newMappings) {
269 mappings.clear();
270 mappings.addAll(newMappings);
271 remapAll();
275 * Hide our private parts from the navigators other browsers.
277 * @throws CoreException
279 public void markTeamPrivateResources() throws CoreException {
280 for (final Object rmObj : mappings) {
281 final RepositoryMapping rm = (RepositoryMapping)rmObj;
282 final IContainer c = rm.getContainer();
283 if (c == null)
284 continue; // Not fully mapped yet?
286 final IResource dotGit = c.findMember(".git");
287 if (dotGit != null) {
288 try {
289 final Repository r = rm.getRepository();
290 final File dotGitDir = dotGit.getLocation().toFile()
291 .getCanonicalFile();
292 if (dotGitDir.equals(r.getDirectory())) {
293 trace("teamPrivate " + dotGit);
294 dotGit.setTeamPrivateMember(true);
296 } catch (IOException err) {
297 throw Activator.error(CoreText.Error_CanonicalFile, err);
304 * @param f
305 * @return true if a resource is protected in this repository
307 public boolean isProtected(final IResource f) {
308 return protectedResources.contains(f);
312 * @param r any workbench resource contained within this project.
313 * @return the mapping for the specified project
315 public RepositoryMapping getRepositoryMapping(IResource r) {
316 try {
317 for (; r != null; r = r.getParent()) {
318 final RepositoryMapping m;
320 if (!r.isAccessible())
321 continue;
322 m = (RepositoryMapping) r.getSessionProperty(MAPPING_KEY);
323 if (m != null)
324 return m;
326 } catch (CoreException err) {
327 Activator.logError("Failed finding RepositoryMapping", err);
329 return null;
332 private void delete() {
333 final File dir = propertyFile().getParentFile();
334 final File[] todel = dir.listFiles();
335 if (todel != null) {
336 for (int k = 0; k < todel.length; k++) {
337 if (todel[k].isFile()) {
338 todel[k].delete();
342 dir.delete();
343 trace("deleteDataFor(" + getProject().getName() + ")");
344 uncache(getProject());
348 * Store information about the repository connection in the workspace
350 * @throws CoreException
352 public void store() throws CoreException {
353 final File dat = propertyFile();
354 final File tmp;
355 boolean ok = false;
357 try {
358 trace("save " + dat);
359 tmp = File.createTempFile("gpd_", ".prop", dat.getParentFile());
360 final FileOutputStream o = new FileOutputStream(tmp);
361 try {
362 final Properties p = new Properties();
363 final Iterator i = mappings.iterator();
364 while (i.hasNext()) {
365 ((RepositoryMapping) i.next()).store(p);
367 p.store(o, "GitProjectData");
368 ok = true;
369 } finally {
370 o.close();
371 if (!ok) {
372 tmp.delete();
375 } catch (IOException ioe) {
376 throw Activator.error(NLS.bind(CoreText.GitProjectData_saveFailed,
377 dat), ioe);
380 dat.delete();
381 if (!tmp.renameTo(dat)) {
382 tmp.delete();
383 throw Activator.error(NLS.bind(CoreText.GitProjectData_saveFailed,
384 dat), null);
388 private File propertyFile() {
389 return new File(getProject()
390 .getWorkingLocation(Activator.getPluginId()).toFile(),
391 "GitProjectData.properties");
394 private GitProjectData load() throws IOException {
395 final File dat = propertyFile();
396 trace("load " + dat);
398 final FileInputStream o = new FileInputStream(dat);
399 try {
400 final Properties p = new Properties();
401 p.load(o);
403 mappings.clear();
404 final Iterator keyItr = p.keySet().iterator();
405 while (keyItr.hasNext()) {
406 final String key = keyItr.next().toString();
407 if (RepositoryMapping.isInitialKey(key)) {
408 mappings.add(new RepositoryMapping(p, key));
411 } finally {
412 o.close();
415 remapAll();
416 return this;
419 private void remapAll() {
420 protectedResources.clear();
421 final Iterator i = mappings.iterator();
422 while (i.hasNext()) {
423 map((RepositoryMapping) i.next());
427 private void map(final RepositoryMapping m) {
428 final IResource r;
429 final File git;
430 final IResource dotGit;
431 IContainer c = null;
433 m.clear();
434 r = getProject().findMember(m.getContainerPath());
435 if (r instanceof IContainer) {
436 c = (IContainer) r;
437 } else {
438 c = (IContainer) r.getAdapter(IContainer.class);
441 if (c == null) {
442 Activator.logError(CoreText.GitProjectData_mappedResourceGone,
443 new FileNotFoundException(m.getContainerPath().toString()));
444 m.clear();
445 return;
447 m.setContainer(c);
449 git = c.getLocation().append(m.getGitDirPath()).toFile();
450 if (!git.isDirectory() || !new File(git, "config").isFile()) {
451 Activator.logError(CoreText.GitProjectData_mappedResourceGone,
452 new FileNotFoundException(m.getContainerPath().toString()));
453 m.clear();
454 return;
457 try {
458 m.setRepository(lookupRepository(git));
459 } catch (IOException ioe) {
460 Activator.logError(CoreText.GitProjectData_mappedResourceGone,
461 new FileNotFoundException(m.getContainerPath().toString()));
462 m.clear();
463 return;
466 m.fireRepositoryChanged();
468 trace("map " + c + " -> " + m.getRepository());
469 try {
470 c.setSessionProperty(MAPPING_KEY, m);
471 } catch (CoreException err) {
472 Activator.logError("Failed to cache RepositoryMapping", err);
475 dotGit = c.findMember(".git");
476 if (dotGit != null && dotGit.getLocation().toFile().equals(git)) {
477 protect(dotGit);
481 private void protect(IResource c) {
482 while (c != null && !c.equals(getProject())) {
483 trace("protect " + c);
484 protectedResources.add(c);
485 c = c.getParent();