Skip to main content
summaryrefslogtreecommitdiffstats
blob: 56b969dc8e7074ef2acd9f6cad32380b2476bacb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
/*******************************************************************************
 * Copyright (c) 2000, 2003 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.team.internal.ui.synchronize;

import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.*;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.core.subscribers.Subscriber;
import org.eclipse.team.core.synchronize.SyncInfo;
import org.eclipse.team.core.synchronize.SyncInfoTree;
import org.eclipse.team.internal.core.Assert;
import org.eclipse.team.internal.core.subscribers.SubscriberSyncInfoCollector;
import org.eclipse.team.internal.ui.Policy;
import org.eclipse.team.internal.ui.TeamUIPlugin;
import org.eclipse.team.ui.synchronize.ISynchronizeManager;
import org.eclipse.team.ui.synchronize.SubscriberParticipant;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
import org.eclipse.ui.progress.IProgressConstants;
import org.eclipse.ui.progress.UIJob;

/**
 * Job to refresh a {@link Subscriber} in the background. The job can be configured
 * to be re-scheduled and run at a specified interval.
 * <p>
 * The job supports a basic workflow for modal/non-modal usage. If the job is
 * run in the foreground (e.g. in a modal progress dialog) the refresh listeners
 * action is invoked immediately after the refresh is completed. Otherwise the refresh
 * listeners action is associated to the job as a <i>goto</i> action. This will
 * allow the user to select the action in the progress view and run it when they
 * choose.
 * </p>
 * @since 3.0
 */
public final class RefreshSubscriberJob extends Job {
	
	/**
	 * Uniquely identifies this type of job. This is used for cancellation.
	 */
	private final static Object FAMILY_ID = new Object();
	
	/**
	 * If true this job will be restarted when it completes 
	 */
	private boolean reschedule = false;
	
	/**
	 * If true a rescheduled refresh job should be retarted when cancelled
	 */
	private boolean restartOnCancel = true; 
	
	/**
	 * The schedule delay used when rescheduling a completed job 
	 */
	private static long scheduleDelay;
	
	/**
	 * The subscribers and resources to refresh.
	 */
	private IResource[] resources;

	/**
	 * The participant that is being refreshed.
	 */
	private SubscriberParticipant participant;
	
	/**
	 * The task name for this refresh. This is usually more descriptive than the
	 * job name.
	 */
	private String taskName;
	
	/**
	 * Refresh started/completed listener for every refresh
	 */
	private static List listeners = new ArrayList(1);
	private static final int STARTED = 1;
	private static final int DONE = 2;
	
	/*
	 * Lock used to sequence refresh jobs
	 */
	private static final ILock lock = Platform.getJobManager().newLock(); 
	
	/*
	 * Constant used for postponement
	 */
	private static final IStatus POSTPONED = new Status(IStatus.CANCEL, TeamUIPlugin.ID, 0, "Scheduled refresh postponed due to conflicting operation", null); //$NON-NLS-1$
	
	/**
	 * Notification for safely notifying listeners of refresh lifecycle.
	 */
	private abstract class Notification implements ISafeRunnable {
		private IRefreshSubscriberListener listener;
		public void handleException(Throwable exception) {
			// don't log the exception....it is already being logged in Platform#run
		}
		public void run(IRefreshSubscriberListener listener) {
			this.listener = listener;
			Platform.run(this);
		}
		public void run() throws Exception {
			notify(listener);
		}
		/**
		 * Subsclasses overide this method to send an event safely to a lsistener
		 * @param listener
		 */
		protected abstract void notify(IRefreshSubscriberListener listener);
	}
	
	/**
	 * Monitor wrapper that will indicate that the job is cancelled 
	 * if the job is blocking another.
	 */
	private class NonblockingProgressMonitor extends ProgressMonitorWrapper {
		private final RefreshSubscriberJob job;
		private long blockTime;
		private static final int THRESHOLD = 250;
		private boolean wasBlocking = false;
		protected NonblockingProgressMonitor(IProgressMonitor monitor, RefreshSubscriberJob job) {
			super(monitor);
			this.job = job;
		}
		public boolean isCanceled() {
			if (super.isCanceled()) {
				return true;
			}
			if (job.shouldReschedule() && job.isBlocking()) {
				if (blockTime == 0) {
					blockTime = System.currentTimeMillis();
				} else if (System.currentTimeMillis() - blockTime > THRESHOLD) {
					// We've been blocking for too long
					wasBlocking = true;
					return true;
				}
			} else {
				blockTime = 0;
			}
			wasBlocking = false;
			return false;
		}
		public boolean wasBlocking() {
			return wasBlocking;
		}
	}
	
	/**
	 * Create a job to refresh the specified resources with the subscriber.
	 * 
	 * @param participant the subscriber participant 
	 * @param name
	 * @param resources
	 * @param subscriber
	 */
	public RefreshSubscriberJob(SubscriberParticipant participant, String jobName, String taskName, IResource[] resources, IRefreshSubscriberListener listener) {
		super(taskName);
		Assert.isNotNull(resources);
		Assert.isNotNull(participant);
		Assert.isNotNull(resources);
		this.resources = resources;
		this.participant = participant;
		this.taskName = jobName;
		setPriority(Job.DECORATE);
		setRefreshInterval(3600 /* 1 hour */);
		
		// Handle restarting of job if it is configured as a scheduled refresh job.
		addJobChangeListener(new JobChangeAdapter() {
			public void done(IJobChangeEvent event) {
				if(shouldReschedule()) {
					IStatus result = event.getResult();
					if(result.getSeverity() == IStatus.CANCEL && ! restartOnCancel) {					
						return;
					}
					long delay = scheduleDelay;
					if (result == POSTPONED) {
						// Restart in 5 seconds
						delay = 5000;
					}
					RefreshSubscriberJob.this.schedule(delay);
					restartOnCancel = true;
				}
			}
		});		
		if(listener != null)
			initialize(listener);
	}
	
	/**
	 * If a collector is available then run the refresh and the background event processing 
	 * within the same progess group.
	 */
	public boolean shouldRun() {
		// Ensure that any progress shown as a result of this refresh occurs hidden in a progress group.
		return getSubscriber() != null;
	}

	public boolean belongsTo(Object family) {	
		if(family instanceof RefreshSubscriberJob) {
			return ((RefreshSubscriberJob)family).getSubscriber() == getSubscriber();
		} else if (family instanceof SubscriberParticipant) {
			return family == participant;
		} else {
			return (family == getFamily() || family == ISynchronizeManager.FAMILY_SYNCHRONIZE_OPERATION);
		}
	}
	
	public static Object getFamily() {
		return FAMILY_ID;
	}
	
	/**
	 * This is run by the job scheduler. A list of subscribers will be refreshed, errors will not stop the job 
	 * and it will continue to refresh the other subscribers.
	 */
	public IStatus run(IProgressMonitor monitor) {
		// Perform a pre-check for auto-build or manual build jobs
		// when auto-refreshing
		if (shouldReschedule() &&
				(isJobInFamilyRunning(ResourcesPlugin.FAMILY_AUTO_BUILD)
				|| isJobInFamilyRunning(ResourcesPlugin.FAMILY_MANUAL_BUILD))) {
			return POSTPONED;		
		}
		// Only allow one refresh job at a time
		// NOTE: It would be cleaner if this was done by a scheduling
		// rule but at the time of writting, it is not possible due to
		// the scheduling rule containment rules.
		// Acquiring lock to ensure only one refresh job is running at a particular time
		boolean acquired = false;
		try {
			while (!acquired) {
				try {
					acquired = lock.acquire(1000);
				} catch (InterruptedException e1) {
					acquired = false;
				}
				Policy.checkCanceled(monitor);
			}
			Subscriber subscriber = getSubscriber();
			IResource[] roots = getResources();
			
			// if there are no resources to refresh, just return
			if(subscriber == null || roots == null) {
				return Status.OK_STATUS;
			}
			SubscriberSyncInfoCollector collector = getCollector();
			RefreshEvent event = new RefreshEvent(reschedule ? IRefreshEvent.SCHEDULED_REFRESH : IRefreshEvent.USER_REFRESH, roots, collector.getSubscriber());
			RefreshChangeListener changeListener = new RefreshChangeListener(collector);
			IStatus status = null;
			NonblockingProgressMonitor wrappedMonitor = null;
			try {
				event.setStartTime(System.currentTimeMillis());
				if(monitor.isCanceled()) {
					return Status.CANCEL_STATUS;
				}
				// Set-up change listener so that we can determine the changes found
				// during this refresh.						
				subscriber.addListener(changeListener);
				// Pre-Notify
				notifyListeners(STARTED, event);
				// Perform the refresh		
				monitor.setTaskName(getName());
				wrappedMonitor = new NonblockingProgressMonitor(monitor, this);
				subscriber.refresh(roots, IResource.DEPTH_INFINITE, wrappedMonitor);
				// Prepare the results
				setProperty(IProgressConstants.KEEPONE_PROPERTY, Boolean.valueOf(! isJobModal()));
			} catch(OperationCanceledException e2) {
				if (monitor.isCanceled()) {
					// The refresh was cancelled by the user
					status = Status.CANCEL_STATUS;
				} else {
					// The refresh was cancelled due to a blockage or a cancelled authentication
					if (wrappedMonitor != null && wrappedMonitor.wasBlocking()) {
						status = POSTPONED;
					} else {
						status = Status.CANCEL_STATUS;
					}
				}
			} catch(TeamException e) {
				status = e.getStatus();
			} finally {
				event.setStopTime(System.currentTimeMillis());
				subscriber.removeListener(changeListener);
				monitor.done();
			}
			
			// Post-Notify
			event.setChanges(changeListener.getChanges());
			if (status == null) {
				status = calculateStatus(event);
			}
			event.setStatus(status);
			notifyListeners(DONE, event);
			return event.getStatus();
		} finally {
			if (acquired) lock.release();
		}
	}
	
	private boolean isJobInFamilyRunning(Object family) {
		Job[] jobs = Platform.getJobManager().find(family);
		if (jobs != null && jobs.length > 0) {
			for (int i = 0; i < jobs.length; i++) {
				Job job = jobs[i];
				if (job.getState() != Job.NONE) {
					return true;
				}
			}
		}
		return false;
	}

	private IStatus calculateStatus(IRefreshEvent event) {
		StringBuffer text = new StringBuffer();
		int code = IStatus.OK;
		SyncInfo[] changes = event.getChanges();
		SubscriberSyncInfoCollector collector = getCollector();
		if (collector != null) {
			int numChanges = refreshedResourcesContainChanges(event);
			if (numChanges > 0) {
				code = IRefreshEvent.STATUS_CHANGES;
				if (changes.length > 0) {
				// New changes found
					String numNewChanges = Integer.toString(event.getChanges().length);
					if (event.getChanges().length == 1) {
							text.append(Policy.bind("RefreshCompleteDialog.newChangesSingular", new Object[]{getName(), numNewChanges})); //$NON-NLS-1$
					} else {
							text.append(Policy.bind("RefreshCompleteDialog.newChangesPlural", new Object[]{getName(), numNewChanges})); //$NON-NLS-1$
						}
				} else {
					// Refreshed resources contain changes
					if (numChanges == 1) {
						text.append(Policy.bind("RefreshCompleteDialog.changesSingular", new Object[]{getName(), new Integer(numChanges)})); //$NON-NLS-1$
					} else {
						text.append(Policy.bind("RefreshCompleteDialog.changesPlural", new Object[]{getName(), new Integer(numChanges)})); //$NON-NLS-1$
					}
				}
			} else {
				// No changes found
				code = IRefreshEvent.STATUS_NO_CHANGES;
				text.append(Policy.bind("RefreshCompleteDialog.6", getName())); //$NON-NLS-1$
			}
			return new Status(IStatus.OK, TeamUIPlugin.ID, code, text.toString(), null);
		}
		return Status.OK_STATUS;
	}
	
	private int refreshedResourcesContainChanges(IRefreshEvent event) {
		int numChanges = 0;
		SubscriberSyncInfoCollector collector = getCollector();
		if (collector != null) {
			SyncInfoTree set = collector.getSyncInfoSet();
			IResource[] resources = event.getResources();
			for (int i = 0; i < resources.length; i++) {
				IResource resource = resources[i];
				SyncInfo[] infos = set.getSyncInfos(resource, IResource.DEPTH_INFINITE);
				if(infos != null && infos.length > 0) {
					numChanges += infos.length;
				}
			}
		}
		return numChanges;
	}
	
	private void initialize(final IRefreshSubscriberListener listener) {
		final IWorkbenchAction[] gotoAction = new IWorkbenchAction[] {null};
		final IWorkbenchAction actionWrapper = new WorkbenchAction() {
			public void run() {
				if(gotoAction[0] != null) {
					gotoAction[0].run();
				}
			}
			public boolean isEnabled() {
				if(gotoAction[0] != null) {
					return gotoAction[0].isEnabled();
				}
				return true;
			}
			
			public String getToolTipText() {
				if(gotoAction[0] != null) {
					return gotoAction[0].getToolTipText();
				}
				return super.getToolTipText();
			}
			
			public void dispose() {
				super.dispose();
				if(gotoAction[0] != null) {
					gotoAction[0].dispose();
				}
			}
		};
		
		IProgressMonitor group = Platform.getJobManager().createProgressGroup();
		group.beginTask(taskName, 100);
		setProgressGroup(group, 80);
		getCollector().setProgressGroup(group, 20);
		setProperty(IProgressConstants.ICON_PROPERTY, participant.getImageDescriptor());
		setProperty(IProgressConstants.ACTION_PROPERTY, actionWrapper);
		setProperty(IProgressConstants.KEEPONE_PROPERTY, Boolean.valueOf(! isJobModal()));
		// Listener delagate
		IRefreshSubscriberListener autoListener = new IRefreshSubscriberListener() {
			public void refreshStarted(IRefreshEvent event) {
				if(listener != null) {
					listener.refreshStarted(event);
				}
			}
			public ActionFactory.IWorkbenchAction refreshDone(IRefreshEvent event) {
				if(listener != null) {
					boolean isModal = isJobModal();
					ActionFactory.IWorkbenchAction runnable = listener.refreshDone(event);
					if(runnable != null) {
					// If the job is being run modally then simply prompt the user immediatly
					if(isModal) {
						if(runnable != null) {
							final IAction[] r = new IAction[] {runnable};
							Job update = new UIJob("") { //$NON-NLS-1$
								public IStatus runInUIThread(IProgressMonitor monitor) {
									r[0].run();
									return Status.OK_STATUS;
								}
							};
							update.setSystem(true);
							update.schedule();
						}
					// If the job is being run in the background, don't interrupt the user and simply update the goto action
					// to perform the results.
					} else {
						gotoAction[0] = runnable;
						actionWrapper.setEnabled(runnable.isEnabled());
						actionWrapper.setToolTipText(runnable.getToolTipText());
						runnable.addPropertyChangeListener(new IPropertyChangeListener() {
							public void propertyChange(PropertyChangeEvent event) {
								if(event.getProperty().equals(IAction.ENABLED)) {
									Boolean bool = (Boolean) event.getNewValue();
									actionWrapper.setEnabled(bool.booleanValue());
								}
							}
						});
					}
					}
					RefreshSubscriberJob.removeRefreshListener(this);
				}
				return null;
			}
		};
		
		if (listener != null) {
			RefreshSubscriberJob.addRefreshListener(autoListener);
		}	
	}
	
	protected IResource[] getResources() {
		return resources;
	}
	
	protected Subscriber getSubscriber() {
		return participant.getSubscriber();
	}
	
	protected SubscriberSyncInfoCollector getCollector() {
		return participant.getSubscriberSyncInfoCollector();
	}
	
	public long getScheduleDelay() {
		return scheduleDelay;
	}
	
	protected void start() {
		if(getState() == Job.NONE) {
			if(shouldReschedule()) {
				schedule(getScheduleDelay());
			}
		}
	}
	
	/**
	 * Specify the interval in seconds at which this job is scheduled.
	 * @param seconds delay specified in seconds
	 */
	public void setRefreshInterval(long seconds) {
		boolean restart = false;
		if(getState() == Job.SLEEPING) {
			restart = true;
			cancel();
		}
		scheduleDelay = seconds * 1000;
		if(restart) {
			start();
		}
	}
	
	/**
	 * Returns the interval of this job in seconds. 
	 * @return
	 */
	public long getRefreshInterval() {
		return scheduleDelay / 1000;
	}
	
	public void setRestartOnCancel(boolean restartOnCancel) {
		this.restartOnCancel = restartOnCancel;
	}
	
	public void setReschedule(boolean reschedule) {
		this.reschedule = reschedule;
	}
	
	public boolean shouldReschedule() {
		return reschedule;
	}
	
	public static void addRefreshListener(IRefreshSubscriberListener listener) {
		synchronized(listeners) {
			if(! listeners.contains(listener)) {
				listeners.add(listener);
			}
		}
	}
	
	public static void removeRefreshListener(IRefreshSubscriberListener listener) {
		synchronized(listeners) {
			listeners.remove(listener);
		}
	}
	
	protected void notifyListeners(final int state, final IRefreshEvent event) {
		// Get a snapshot of the listeners so the list doesn't change while we're firing
		IRefreshSubscriberListener[] listenerArray;
		synchronized (listeners) {
			listenerArray = (IRefreshSubscriberListener[]) listeners.toArray(new IRefreshSubscriberListener[listeners.size()]);
		}
		// Notify each listener in a safe manner (i.e. so their exceptions don't kill us)
		for (int i = 0; i < listenerArray.length; i++) {
			IRefreshSubscriberListener listener = listenerArray[i];
			Notification notification = new Notification() {
				protected void notify(IRefreshSubscriberListener listener) {
					switch (state) {
						case STARTED:
							listener.refreshStarted(event);
							break;
						case DONE:
							listener.refreshDone(event);
							break;
						default:
							break;
					}
				}
			};
			notification.run(listener);
		}
	}
	
	private boolean isJobModal() {
		Boolean isModal = (Boolean)getProperty(IProgressConstants.PROPERTY_IN_DIALOG);
		if(isModal == null) return false;
		return isModal.booleanValue();
	}
}

Back to the top