Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: ad38e107eee838c2119dbe48fd52e13bb0ce619a (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
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
/*******************************************************************************
 * Copyright (c) 2000, 2017 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Eugene Kuleshov (eu@md.pp.ru) - Bug 138152 Improve sync job status reporting
 *******************************************************************************/
package org.eclipse.team.internal.ui.synchronize;

import java.util.ArrayList;
import java.util.List;

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.dialogs.ErrorDialog;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.subscribers.Subscriber;
import org.eclipse.team.internal.ui.*;
import org.eclipse.team.ui.synchronize.*;
import org.eclipse.ui.actions.ActionFactory;
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 work flow 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 abstract class RefreshParticipantJob 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 restarted when canceled
	 */
	private boolean restartOnCancel = true;

	/**
	 * The schedule delay used when rescheduling a completed job
	 */
	private static long scheduleDelay;

	/**
	 * The participant that is being refreshed.
	 */
	private ISynchronizeParticipant 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<IRefreshSubscriberListener> 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 = Job.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$

	/*
	 * Action wrapper which allows the goto action
	 * to be set later. It also handles errors
	 * that have occurred during the refresh
	 */
	private final class GotoActionWrapper extends WorkbenchAction {
        private ActionFactory.IWorkbenchAction gotoAction;
        private IStatus status;
        @Override
		public void run() {
            if (status != null && !status.isOK()) {
                ErrorDialog.openError(Utils.getShell(null), null, TeamUIMessages.RefreshSubscriberJob_3, status);
            } else if(gotoAction != null) {
        		gotoAction.run();
        	}
        }
        @Override
		public boolean isEnabled() {
        	if(gotoAction != null) {
        		return gotoAction.isEnabled();
        	}
        	return true;
        }
        @Override
		public String getText() {
        	if(gotoAction != null) {
        		return gotoAction.getText();
        	}
        	return null;
        }
        @Override
		public String getToolTipText() {
            if (status != null && !status.isOK()) {
                return status.getMessage();
            }
        	if(gotoAction != null) {
        		return gotoAction.getToolTipText();
        	}
        	return Utils.shortenText(SynchronizeView.MAX_NAME_LENGTH, RefreshParticipantJob.this.getName());
        }
        @Override
		public void dispose() {
        	super.dispose();
        	if(gotoAction != null) {
        		gotoAction.dispose();
        	}
        }
        public void setGotoAction(ActionFactory.IWorkbenchAction gotoAction) {
            this.gotoAction = gotoAction;
			setEnabled(isEnabled());
			setToolTipText(getToolTipText());
			gotoAction.addPropertyChangeListener(event -> {
				if(event.getProperty().equals(IAction.ENABLED)) {
					Boolean bool = (Boolean) event.getNewValue();
					GotoActionWrapper.this.setEnabled(bool.booleanValue());
				}
			});
        }
        public void setStatus(IStatus status) {
            this.status = status;
        }
    }

    /**
	 * Notification for safely notifying listeners of refresh lifecycle.
	 */
	private abstract class Notification implements ISafeRunnable {
		private IRefreshSubscriberListener listener;
		@Override
		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;
			SafeRunner.run(this);
		}
		@Override
		public void run() throws Exception {
			notify(listener);
		}
		/**
		 * Subclasses override this method to send an event safely to a listener
		 * @param listener
		 */
		protected abstract void notify(IRefreshSubscriberListener listener);
	}

	/**
	 * Monitor wrapper that will indicate that the job is canceled
	 * if the job is blocking another.
	 */
	private class NonblockingProgressMonitor extends ProgressMonitorWrapper {
		private final RefreshParticipantJob job;
		private long blockTime;
		private static final int THRESHOLD = 250;
		private boolean wasBlocking = false;
		protected NonblockingProgressMonitor(IProgressMonitor monitor, RefreshParticipantJob job) {
			super(monitor);
			this.job = job;
		}
		@Override
		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;
		}
	}

	public static interface IChangeDescription {
		int getChangeCount();
	}

	/**
	 * Create a job to refresh the specified resources with the subscriber.
	 *
	 * @param participant the subscriber participant
	 * @param jobName
	 * @param taskName
	 * @param listener
	 */
	public RefreshParticipantJob(ISynchronizeParticipant participant, String jobName, String taskName, IRefreshSubscriberListener listener) {
		super(jobName);
		Assert.isNotNull(participant);
		this.participant = participant;
		this.taskName = taskName;
		setPriority(Job.DECORATE);
		setRefreshInterval(3600 /* 1 hour */);

		// Handle restarting of job if it is configured as a scheduled refresh job.
		addJobChangeListener(new JobChangeAdapter() {
			@Override
			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;
					}
					RefreshParticipantJob.this.schedule(delay);
					restartOnCancel = true;
				}
			}
		});
		if(listener != null)
			initialize(listener);
	}

	@Override
	public boolean belongsTo(Object family) {
		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.
	 */
	@Override
	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 writing, 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);
			}

			IChangeDescription changeDescription = createChangeDescription();
			RefreshEvent event = new RefreshEvent(reschedule ? IRefreshEvent.SCHEDULED_REFRESH : IRefreshEvent.USER_REFRESH, participant, changeDescription);
			IStatus status = null;
			NonblockingProgressMonitor wrappedMonitor = null;
			try {
				event.setStartTime(System.currentTimeMillis());
				if(monitor.isCanceled()) {
					return Status.CANCEL_STATUS;
				}
				// Pre-Notify
				notifyListeners(STARTED, event);
				// Perform the refresh
				monitor.setTaskName(getName());
				wrappedMonitor = new NonblockingProgressMonitor(monitor, this);
				doRefresh(changeDescription, wrappedMonitor);
				// Prepare the results
				setProperty(IProgressConstants.KEEPONE_PROPERTY, Boolean.valueOf(! isJobModal()));
			} catch(OperationCanceledException e2) {
				if (monitor.isCanceled()) {
					// The refresh was canceled by the user
					status = Status.CANCEL_STATUS;
				} else {
					// The refresh was canceled due to a blockage or a canceled authentication
					if (wrappedMonitor != null && wrappedMonitor.wasBlocking()) {
						status = POSTPONED;
					} else {
						status = Status.CANCEL_STATUS;
					}
				}
			} catch(CoreException e) {
			    // Determine the status to be returned and the GOTO action
			    status = e.getStatus();
			    if (!isUser()) {
		            // Use the GOTO action to show the error and return OK
		            Object prop = getProperty(IProgressConstants.ACTION_PROPERTY);
		            if (prop instanceof GotoActionWrapper) {
		                GotoActionWrapper wrapper = (GotoActionWrapper)prop;
		                wrapper.setStatus(e.getStatus());
		                status = new Status(IStatus.OK, TeamUIPlugin.ID, IStatus.OK, e.getStatus().getMessage(), e);
		            }
			    }
		        if (!isUser() && status.getSeverity() == IStatus.ERROR) {
		            // Never prompt for errors on non-user jobs
		            setProperty(IProgressConstants.NO_IMMEDIATE_ERROR_PROMPT_PROPERTY, Boolean.TRUE);
		        }
			} finally {
				event.setStopTime(System.currentTimeMillis());
			}

			// Post-Notify
			if (status == null) {
				status = calculateStatus(event);
			}
			event.setStatus(status);
			notifyListeners(DONE, event);
			if (event.getChangeDescription().getChangeCount() > 0) {
				if (participant instanceof AbstractSynchronizeParticipant) {
					AbstractSynchronizeParticipant asp = (AbstractSynchronizeParticipant) participant;
					asp.firePropertyChange(participant, ISynchronizeParticipant.P_CONTENT, null, event.getChangeDescription());
				}
			}
			return event.getStatus();
		} finally {
			if (acquired) lock.release();
            monitor.done();
		}
	}

	protected abstract void doRefresh(IChangeDescription changeListener, IProgressMonitor monitor) throws CoreException;

	/**
	 * Return the total number of changes covered by the resources
	 * of this job.
	 * @return the total number of changes covered by the resources
	 * of this job
	 */
	protected abstract int getChangeCount();

	protected abstract int getIncomingChangeCount();
	protected abstract int getOutgoingChangeCount();

	private boolean isJobInFamilyRunning(Object family) {
		Job[] jobs = Job.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) {
		StringBuilder text = new StringBuilder();
		int code = IStatus.OK;
		int changeCount = event.getChangeDescription().getChangeCount();
		int numChanges = getChangeCount();
		if (numChanges > 0) {
			code = IRefreshEvent.STATUS_CHANGES;

			int incomingChanges = getIncomingChangeCount();
             String numIncomingChanges = incomingChanges==0 ? ""  //$NON-NLS-1$
                 : NLS.bind(TeamUIMessages.RefreshCompleteDialog_incomingChanges, Integer.toString(incomingChanges));

			int outgoingChanges = getOutgoingChangeCount();
			String numOutgoingChanges = outgoingChanges==0 ? ""  //$NON-NLS-1$
                : NLS.bind(TeamUIMessages.RefreshCompleteDialog_outgoingChanges, Integer.toString(outgoingChanges));

			String sep = incomingChanges>0 && outgoingChanges>0 ? "; " : "";  //$NON-NLS-1$ //$NON-NLS-2$

			if (changeCount > 0) {
			// New changes found
				code = IRefreshEvent.STATUS_NEW_CHANGES;
				String numNewChanges = Integer.toString(changeCount);
				if (changeCount == 1) {
				    text.append(NLS.bind(TeamUIMessages.RefreshCompleteDialog_newChangesSingular, (new Object[]{getName(), numNewChanges, numIncomingChanges, sep, numOutgoingChanges})));
				} else {
				    text.append(NLS.bind(TeamUIMessages.RefreshCompleteDialog_newChangesPlural, (new Object[]{getName(), numNewChanges, numIncomingChanges, sep, numOutgoingChanges})));
				}
			} else {
				// Refreshed resources contain changes
				if (numChanges == 1) {
					text.append(NLS.bind(TeamUIMessages.RefreshCompleteDialog_changesSingular, (new Object[]{getName(), Integer.valueOf(numChanges), numIncomingChanges, sep, numOutgoingChanges})));
				} else {
					text.append(NLS.bind(TeamUIMessages.RefreshCompleteDialog_changesPlural, (new Object[]{getName(), Integer.valueOf(numChanges), numIncomingChanges, sep, numOutgoingChanges})));
				}
			}
		} else {
			// No changes found
			code = IRefreshEvent.STATUS_NO_CHANGES;
			text.append(NLS.bind(TeamUIMessages.RefreshCompleteDialog_6, new String[] { getName() }));
		}
		return new Status(IStatus.OK, TeamUIPlugin.ID, code, text.toString(), null);
	}

	private void initialize(final IRefreshSubscriberListener listener) {
		final GotoActionWrapper actionWrapper = new GotoActionWrapper();

		IProgressMonitor group = Job.getJobManager().createProgressGroup();
		group.beginTask(taskName, 100);
		setProgressGroup(group, 80);
		handleProgressGroupSet(group, 20);
		setProperty(IProgressConstants.ICON_PROPERTY, participant.getImageDescriptor());
		setProperty(IProgressConstants.ACTION_PROPERTY, actionWrapper);
		setProperty(IProgressConstants.KEEPONE_PROPERTY, Boolean.valueOf(! isJobModal()));
		// Listener delegate
		IRefreshSubscriberListener autoListener = new IRefreshSubscriberListener() {
			@Override
			public void refreshStarted(IRefreshEvent event) {
				if(listener != null) {
					listener.refreshStarted(event);
				}
			}
			@Override
			public ActionFactory.IWorkbenchAction refreshDone(IRefreshEvent event) {
				if(listener != null) {
					boolean isModal = isJobModal();
					event.setIsLink(!isModal);
					final ActionFactory.IWorkbenchAction runnable = listener.refreshDone(event);
					if(runnable != null) {
						// If the job is being run modally then simply prompt the user immediately
						if(isModal) {
							if(runnable != null) {
								Job update = new UIJob("") { //$NON-NLS-1$
									@Override
									public IStatus runInUIThread(IProgressMonitor monitor) {
									    runnable.run();
										return Status.OK_STATUS;
									}
								};
								update.setSystem(true);
								update.schedule();
							}
						} else {
							// If the job is being run in the background, don't interrupt the user and simply update the goto action
							// to perform the results.
							actionWrapper.setGotoAction(runnable);
						}
					}
					RefreshParticipantJob.removeRefreshListener(this);
				}
				return null;
			}
		};

		if (listener != null) {
			RefreshParticipantJob.addRefreshListener(autoListener);
		}
	}

	/**
	 * The progress group of this job has been set. Any subclasses should
	 * assign this group to any additional jobs they use to collect
	 * changes from the refresh.
	 * @param group a progress group
	 * @param ticks the ticks for the change collection job
	 */
	protected abstract void handleProgressGroupSet(IProgressMonitor group, int ticks);

	protected abstract IChangeDescription createChangeDescription();

	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();
		}
	}

	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 = 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() {
				@Override
				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();
	}

	public ISynchronizeParticipant getParticipant() {
		return participant;
	}
}

Back to the top