1 /*******************************************************************************
2  * Copyright (c) 2000, 2017 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.compare;
15 
16 import java.text.DateFormat;
17 import java.text.MessageFormat;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Calendar;
21 import java.util.Collections;
22 import java.util.Date;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.Iterator;
26 import java.util.List;
27 import java.util.ResourceBundle;
28 
29 import org.eclipse.compare.internal.CompareContainer;
30 import org.eclipse.compare.internal.CompareUIPlugin;
31 import org.eclipse.compare.internal.ResizableDialog;
32 import org.eclipse.compare.internal.StructureCreatorDescriptor;
33 import org.eclipse.compare.internal.Utilities;
34 import org.eclipse.compare.structuremergeviewer.DiffNode;
35 import org.eclipse.compare.structuremergeviewer.ICompareInput;
36 import org.eclipse.compare.structuremergeviewer.IStructureComparator;
37 import org.eclipse.compare.structuremergeviewer.IStructureCreator;
38 import org.eclipse.core.runtime.Assert;
39 import org.eclipse.core.runtime.CoreException;
40 import org.eclipse.jface.dialogs.IDialogConstants;
41 import org.eclipse.jface.resource.ImageDescriptor;
42 import org.eclipse.jface.viewers.ISelection;
43 import org.eclipse.jface.viewers.IStructuredSelection;
44 import org.eclipse.jface.viewers.Viewer;
45 import org.eclipse.swt.SWT;
46 import org.eclipse.swt.events.SelectionAdapter;
47 import org.eclipse.swt.events.SelectionEvent;
48 import org.eclipse.swt.graphics.Image;
49 import org.eclipse.swt.layout.GridData;
50 import org.eclipse.swt.widgets.Button;
51 import org.eclipse.swt.widgets.Composite;
52 import org.eclipse.swt.widgets.Control;
53 import org.eclipse.swt.widgets.Display;
54 import org.eclipse.swt.widgets.Item;
55 import org.eclipse.swt.widgets.Label;
56 import org.eclipse.swt.widgets.Shell;
57 import org.eclipse.swt.widgets.Table;
58 import org.eclipse.swt.widgets.TableItem;
59 import org.eclipse.swt.widgets.Tree;
60 import org.eclipse.swt.widgets.TreeItem;
61 import org.eclipse.swt.widgets.Widget;
62 
63 /**
64  * A dialog where one input element can be compared against
65  * a list of historic variants (editions) of the same input element.
66  * The dialog can be used to implement functions like "Compare/Replace with Version" or
67  * "Compare/Replace from Local History" on workspace resources.
68  * <p>
69  * In addition it is possible to specify a subsection of the input element
70  * (e.g. a method in a Java source file) by means of a "path".
71  * In this case the dialog compares only the subsection (as specified by the path)
72  * with the corresponding subsection in the list of editions.
73  * Only those editions are shown where the subsection differs from the same subsection in
74  * another edition thereby minimizing the number of presented variants.
75  * This functionality can be used to implement "Replace from Local History"
76  * for the Java language.
77  * <p>
78  * Subsections of an input element are determined by first finding an
79  * <code>IStructureCreator</code> for the input's type.
80  * Then the method <code>locate</code> is used to extract the subsection.
81  * <p>
82  * Each edition (variant in the list of variants) must implement the <code>IModificationDate</code> interface
83  * so that the dialog can sort the editions and present them in a tree structure where every
84  * node corresponds one day.
85  * <p>
86  * The functionality is surfaced in a single function <code>selectEdition</code>.
87  * <p>
88  * Clients may instantiate this class; it is not intended to be subclassed.
89  * </p>
90  *
91  * @see IModificationDate
92  * @see ITypedElement
93  *
94  * @deprecated Use an <code>org.eclipse.team.ui.history.IHistoryPageSource</code> in conjunction with
95  * the <code>org.eclipse.team.ui.history.IHistoryView</code> or a <code>HistoryPageCompareEditorInput</code>.
96  * For sub-file elements, a <code>org.eclipse.team.ui.history.ElementLocalHistoryPageSource</code> can be used.
97  * @noextend This class is not intended to be subclassed by clients.
98  */
99 @Deprecated
100 public class EditionSelectionDialog extends ResizableDialog {
101 
102 	/**
103 	 * An item in an underlying edition.
104 	 */
105 	private static class Pair {
106 
107 		private ITypedElement fEdition;
108 		private ITypedElement fItem;
109 		private String fContent;
110 		private IStructureCreator fStructureCreator;
111 		private boolean fHasError= false;
112 
Pair(IStructureCreator structureCreator, ITypedElement edition, ITypedElement item)113 		Pair(IStructureCreator structureCreator, ITypedElement edition, ITypedElement item) {
114 			fStructureCreator= structureCreator;
115 			fEdition= edition;
116 			fItem= item;
117 		}
118 
Pair(IStructureCreator structureCreator, ITypedElement edition)119 		Pair(IStructureCreator structureCreator, ITypedElement edition) {
120 			this(structureCreator, edition, edition);
121 		}
122 
getEdition()123 		ITypedElement getEdition() {
124 			return fEdition;
125 		}
126 
getItem()127 		ITypedElement getItem() {
128 			return fItem;
129 		}
130 
131 		/*
132 		 * The content is lazily loaded
133 		 */
getContent()134 		private String getContent() {
135 			if (fContent == null) {
136 				if (fStructureCreator != null)
137 					fContent= fStructureCreator.getContents(fItem, false);
138 				else {
139 					if (fItem instanceof IStreamContentAccessor) {
140 						IStreamContentAccessor sca= (IStreamContentAccessor) fItem;
141 						try {
142 							fContent= Utilities.readString(sca);
143 						} catch (CoreException ex) {
144 							// NeedWork
145 							CompareUIPlugin.log(ex);
146 						}
147 					}
148 				}
149 				if (fContent == null)
150 					fContent= ""; //$NON-NLS-1$
151 			}
152 			return fContent;
153 		}
154 
155 		@Override
equals(Object other)156 		public boolean equals(Object other) {
157 			if (other != null && other.getClass() == getClass()) {
158 				if (getContent().equals(((Pair)other).getContent()))
159 					return true;
160 			}
161 			return super.equals(other);
162 		}
163 
164 		@Override
hashCode()165 		public int hashCode() {
166 			return getContent().hashCode();
167 		}
168 	}
169 
170 	// Configuration options
171 	private CompareConfiguration fCompareConfiguration;
172 	private ArrayList<Object> fArrayList= new ArrayList<>();
173 	/** use a side-by-side compare viewer */
174 	private boolean fCompare= true;
175 	/** show target on right hand side */
176 	private boolean fTargetIsRight= false;
177 	/** hide entries which have identical content */
178 	private boolean fHideIdentical= true;
179 	/** add mode if true, otherwise replace mode */
180 	private boolean fAddMode= false;
181 	/** compare mode if true, otherwise replace/add mode */
182 	private boolean fCompareMode= false;
183 	/** perform structure compare on editions */
184 	private boolean fStructureCompare= false;
185 	/** allow for multiple selection */
186 	private boolean fMultiSelect= false;
187 
188 	/**
189 	 * Maps from members to their corresponding editions.
190 	 * Has only a single entry if dialog is used in "Replace" (and not "Add") mode.
191 	 */
192 	private HashMap<ITypedElement, List<Pair>> fMemberEditions;
193 	/**
194 	 * Maps from members to their corresponding selected edition.
195 	 */
196 	private HashMap<List, ITypedElement> fMemberSelection;
197 	/** The editions of the current selected member */
198 	private List fCurrentEditions;
199 	private Thread fThread;
200 	private Pair fTargetPair;
201 	/** The selected edition in the edition viewer */
202 	private ITypedElement fSelectedItem;
203 	private String fTitleArg;
204 	private Image fTitleImage;
205 
206 	// SWT controls
207 	private CompareViewerSwitchingPane fContentPane;
208 	private Button fCommitButton;
209 	private Table fMemberTable;
210 	private CompareViewerPane fMemberPane;
211 	private Tree fEditionTree;
212 	private CompareViewerPane fEditionPane;
213 	private Image fDateImage;
214 	private Image fTimeImage;
215 	private CompareViewerSwitchingPane fStructuredComparePane;
216 	private Label statusLabel;
217 
218 	/**
219 	 * Creates a new modal, resizable dialog.
220 	 * Various titles, icons, and labels are configured from the given resource bundle.
221 	 * The following resource keys are used:
222 	 * <pre>
223 	 *	key         type          description
224 	 *	title       String        dialog title
225 	 *	width       Integer       initial width of dialog
226 	 *	height      Integer       initial height of dialog
227 	 *	treeTitleFormat   MessageFormat pane title for edition tree; arg 0 is the target
228 	 *	dateIcon    String        icon for node in edition tree; path relative to plug-in
229 	 *	timeIcon    String        icon for leaf in edition tree; path relative to plug-in
230 	 *	todayFormat MessageFormat format string if date is todays date; arg 0 is date
231 	 *	yesterdayFormat MessageFormat format string if date is yesterdays date; arg 0 is date
232 	 *	dayFormat   MessageFormat format string if date is any other date; arg 0 is date
233 	 *	editionLabel String       label for editions side of compare viewer; arg 0 is the date
234 	 *	targetLabel  String       label for target side of compare viewer
235 	 *  buttonLabel  String       label for OK button; default is IDialogConstants.OK_LABEL
236 	 * </pre>
237 	 *
238 	 * @param parent if not <code>null</code> the new dialog stays on top of this parent shell
239 	 * @param bundle <code>ResourceBundle</code> to configure the dialog
240 	 */
EditionSelectionDialog(Shell parent, ResourceBundle bundle)241 	public EditionSelectionDialog(Shell parent, ResourceBundle bundle) {
242 		super(parent, bundle);
243 	}
244 
getCompareConfiguration()245 	private CompareConfiguration getCompareConfiguration() {
246 		if (fCompareConfiguration == null) {
247 			fCompareConfiguration= new CompareConfiguration();
248 			fCompareConfiguration.setLeftEditable(false);
249 			fCompareConfiguration.setRightEditable(false);
250 			fCompareConfiguration.setContainer(new CompareContainer() {
251 				@Override
252 				public void setStatusMessage(String message) {
253 					if (statusLabel != null && !statusLabel.isDisposed()) {
254 						if (message == null) {
255 							statusLabel.setText(""); //$NON-NLS-1$
256 						} else {
257 							statusLabel.setText(message);
258 						}
259 					}
260 				}
261 			});
262 		}
263 		return fCompareConfiguration;
264 	}
265 
266 	/**
267 	 * Sets the help context for this dialog.
268 	 *
269 	 * @param contextId the help context id.
270 	 * @since 3.2
271 	 */
272 	@Override
setHelpContextId(String contextId)273 	public void setHelpContextId(String contextId) {
274 		super.setHelpContextId(contextId);
275 	}
276 
277 	/**
278 	 * Sets an additional and optional argument for the edition pane's title.
279 	 *
280 	 * @param titleArgument an optional argument for the edition pane's title
281 	 * @since 2.0
282 	 */
setEditionTitleArgument(String titleArgument)283 	public void setEditionTitleArgument(String titleArgument) {
284 		fTitleArg= titleArgument;
285 	}
286 
287 	/**
288 	 * Sets an optional image for the edition pane's title.
289 	 *
290 	 * @param titleImage an optional image for the edition pane's title
291 	 * @since 2.0
292 	 */
setEditionTitleImage(Image titleImage)293 	public void setEditionTitleImage(Image titleImage) {
294 		fTitleImage= titleImage;
295 	}
296 
297 	/**
298 	 * Select the previous edition (presenting a UI).
299 	 *
300 	 * @param target the input object against which the editions are compared; must not be <code>null</code>
301 	 * @param inputEditions the list of editions (element type: <code>ITypedElement</code>s)
302 	 * @param ppath If <code>null</code> dialog shows full input; if non <code>null</code> it extracts a subsection
303 	 * @return returns the selected edition or <code>null</code> if error occurred.
304 	 * The returned <code>ITypedElement</code> is one of the original editions
305 	 * if <code>path</code> was <code>null</code>; otherwise
306 	 * it is an <code>ITypedElement</code> returned from <code>IStructureCreator.locate(path, item)</code>
307 	 * @since 2.0
308 	 */
selectPreviousEdition(final ITypedElement target, ITypedElement[] inputEditions, Object ppath)309 	public ITypedElement selectPreviousEdition(final ITypedElement target, ITypedElement[] inputEditions, Object ppath) {
310 		Assert.isNotNull(target);
311 		fTargetPair= new Pair(null, target);
312 
313 		// sort input editions
314 		final int count= inputEditions.length;
315 		final IModificationDate[] editions= new IModificationDate[count];
316 		for (int i= 0; i < count; i++)
317 			editions[i]= (IModificationDate) inputEditions[i];
318 		if (count > 1)
319 			internalSort(editions);
320 
321 		// find StructureCreator if ppath is not null
322 		IStructureCreator structureCreator= null;
323 		if (ppath != null) {
324 			String type= target.getType();
325 			StructureCreatorDescriptor scd= CompareUIPlugin.getDefault().getStructureCreator(type);
326 			if (scd != null)
327 				structureCreator= scd.createStructureCreator();
328 		}
329 
330 		if (fAddMode) {
331 			// does not work in add mode
332 			return null;
333 		}
334 
335 		if (structureCreator != null) {
336 			Pair pair= createPair(structureCreator, ppath, target);
337 			if (pair != null)
338 				fTargetPair= pair;
339 			else
340 				ppath= null;	// couldn't extract item because of error
341 		}
342 
343 		// from front (newest) to back (oldest)
344 		for (int i= 0; i < count; i++) {
345 
346 			ITypedElement edition= (ITypedElement) editions[i];
347 			Pair pair= null;
348 
349 			if (structureCreator != null && ppath != null) {
350 				// extract sub element from edition
351 				pair= createPair(structureCreator, ppath, edition);
352 			} else {
353 				pair= new Pair(null, edition);
354 			}
355 
356 			if (pair != null && pair.fHasError)
357 				return null;
358 
359 			if (pair != null && !fTargetPair.equals(pair)) {
360 				return pair.fItem;
361 			}
362 		}
363 
364 		// nothing found
365 		return null;
366 	}
367 
368 	/**
369 	 * Presents this modal dialog with the functionality described in the class comment above.
370 	 *
371 	 * @param target the input object against which the editions are compared; must not be <code>null</code>
372 	 * @param inputEditions the list of editions (element type: <code>ITypedElement</code>s)
373 	 * @param ppath If <code>null</code> dialog shows full input; if non <code>null</code> it extracts a subsection
374 	 * @return returns the selected edition or <code>null</code> if dialog was cancelled.
375 	 * The returned <code>ITypedElement</code> is one of the original editions
376 	 * if <code>path</code> was <code>null</code>; otherwise
377 	 * it is an <code>ITypedElement</code> returned from <code>IStructureCreator.locate(path, item)</code>
378 	 */
selectEdition(final ITypedElement target, ITypedElement[] inputEditions, Object ppath)379 	public ITypedElement selectEdition(final ITypedElement target, ITypedElement[] inputEditions, Object ppath) {
380 
381 		Assert.isNotNull(target);
382 		fTargetPair= new Pair(null, target);
383 
384 		// sort input editions
385 		final int count= inputEditions.length;
386 		final IModificationDate[] editions= new IModificationDate[count];
387 		for (int i= 0; i < count; i++)
388 			editions[i]= (IModificationDate) inputEditions[i];
389 		if (count > 1)
390 			internalSort(editions);
391 
392 		// find StructureCreator if ppath is not null
393 		IStructureCreator structureCreator= null;
394 		if (ppath != null) {
395 			String type= target.getType();
396 			StructureCreatorDescriptor scd= CompareUIPlugin.getDefault().getStructureCreator(type);
397 			if (scd != null)
398 				structureCreator= scd.createStructureCreator();
399 		}
400 
401 		if (!fAddMode) {
402 			// replace mode
403 
404 			if (structureCreator != null) {
405 				Pair pair= createPair(structureCreator, ppath, target);
406 				if (pair != null)
407 					fTargetPair= pair;
408 				else
409 					ppath= null;	// couldn't extract item because of error
410 			}
411 
412 			// set the left and right labels for the compare viewer
413 			String targetLabel= getTargetLabel(target, fTargetPair.getItem());
414 			if (fTargetIsRight)
415 				getCompareConfiguration().setRightLabel(targetLabel);
416 			else
417 				getCompareConfiguration().setLeftLabel(targetLabel);
418 
419 			if (structureCreator != null && ppath != null) {	// extract sub element
420 
421 				final IStructureCreator sc= structureCreator;
422 				final Object path= ppath;
423 
424 				// construct the comparer thread
425 				// and perform the background extract
426 				fThread= new Thread() {
427 					@Override
428 					public void run() {
429 
430 						// from front (newest) to back (oldest)
431 						for (int i= 0; i < count; i++) {
432 
433 							if (fEditionTree == null || fEditionTree.isDisposed())
434 								break;
435 							ITypedElement edition= (ITypedElement) editions[i];
436 
437 							// extract sub element from edition
438 							Pair pair= createPair(sc, path, edition);
439 							if (pair != null)
440 								sendPair(pair);
441 						}
442 						sendPair(null);
443 					}
444 				};
445 			} else {
446 				// create tree widget
447 				create();
448 
449 				// from front (newest) to back (oldest)
450 				for (int i= 0; i < count; i++)
451 					addMemberEdition(new Pair(null, (ITypedElement) editions[i]));
452 			}
453 
454 		} else {
455 			// add mode
456 			final Object container= ppath;
457 			Assert.isNotNull(container);
458 
459 			if (structureCreator == null)
460 				return null;	// error
461 
462 			// extract all elements of container
463 			final HashSet<Object> current= new HashSet<>();
464 			IStructureComparator sco= structureCreator.locate(container, target);
465 			if (sco != null) {
466 				Object[] children= sco.getChildren();
467 				if (children != null)
468 					Collections.addAll(current, children);
469 			}
470 
471 			final IStructureCreator sc= structureCreator;
472 
473 			// construct the comparer thread
474 			// and perform the background extract
475 			fThread= new Thread() {
476 				@Override
477 				public void run() {
478 
479 					// from front (newest) to back (oldest)
480 					for (int i= 0; i < count; i++) {
481 
482 						if (fEditionTree == null || fEditionTree.isDisposed())
483 							break;
484 						ITypedElement edition= (ITypedElement) editions[i];
485 
486 						IStructureComparator sco2= sc.locate(container, edition);
487 						if (sco2 != null) {
488 							Object[] children= sco2.getChildren();
489 							if (children != null) {
490 								for (Object c : children) {
491 									ITypedElement child = (ITypedElement) c;
492 									if (!current.contains(child))
493 										sendPair(new Pair(sc, edition, child));
494 								}
495 							}
496 						}
497 					}
498 					sendPair(null);
499 				}
500 			};
501 		}
502 
503 		open();
504 
505 		if (getReturnCode() == OK)
506 			return fSelectedItem;
507 		return null;
508 	}
509 
createPair(IStructureCreator sc, Object path, ITypedElement input)510 	private Pair createPair(IStructureCreator sc, Object path, ITypedElement input) {
511 		IStructureComparator scmp= sc.locate(path, input);
512 		if (scmp == null && sc.getStructure(input) == null) {	// parse error
513 			Pair p= new Pair(sc, input);
514 			p.fHasError= true;
515 			return p;
516 		}
517 		if (scmp instanceof ITypedElement)
518 			return new Pair(sc, input, (ITypedElement) scmp);
519 		return null;
520 	}
521 
522 	/**
523 	 * Controls whether identical entries are shown or not (default).
524 	 * This method must be called before <code>selectEdition</code>.
525 	 *
526 	 * @param hide if true identical entries are hidden; otherwise they are shown.
527 	 * @since 2.0
528 	 */
setHideIdenticalEntries(boolean hide)529 	public void setHideIdenticalEntries(boolean hide) {
530 		fHideIdentical= hide;
531 	}
532 
533 	/**
534 	 * Controls whether workspace target is on the left (the default) or right hand side.
535 	 *
536 	 * @param isRight if true target is shown on right hand side.
537 	 * @since 2.0
538 	 */
setTargetIsRight(boolean isRight)539 	public void setTargetIsRight(boolean isRight) {
540 		fTargetIsRight= isRight;
541 	}
542 
543 	/**
544 	 * Controls whether the <code>EditionSelectionDialog</code> is in 'add' mode
545 	 * or 'replace' mode (the default).
546 	 *
547 	 * @param addMode if true dialog is in 'add' mode.
548 	 * @since 2.0
549 	 */
setAddMode(boolean addMode)550 	public void setAddMode(boolean addMode) {
551 		fAddMode= addMode;
552 		fMultiSelect= addMode;
553 	}
554 
555 	/**
556 	 * Controls whether the <code>EditionSelectionDialog</code> is in 'compare' mode
557 	 * or 'add/replace' (the default) mode.
558 	 *
559 	 * @param compareMode if true dialog is in 'add' mode.
560 	 * @since 2.0
561 	 */
setCompareMode(boolean compareMode)562 	public void setCompareMode(boolean compareMode) {
563 		fCompareMode= compareMode;
564 		fStructureCompare= fCompareMode && !fAddMode;
565 	}
566 
567 	/**
568 	 * Returns the input target that has been specified with the most recent call
569 	 * to <code>selectEdition</code>. If a not <code>null</code> path was specified this method
570 	 * returns a subsection of this target (<code>IStructureCreator.locate(path, target)</code>)
571 	 * instead of the input target.
572 	 * <p>
573 	 * For example if the <code>target</code> is a Java compilation unit and <code>path</code> specifies
574 	 * a method, the value returned from <code>getTarget</code> will be the method not the compilation unit.
575 	 *
576 	 * @return the last specified target or a subsection thereof.
577 	 */
getTarget()578 	public ITypedElement getTarget() {
579 		return fTargetPair.getItem();
580 	}
581 
582 	/**
583 	 * Returns the editions that have been selected with the most
584 	 * recent call to <code>selectEdition</code>.
585 	 *
586 	 * @return the selected editions as an array.
587 	 * @since 2.1
588 	 */
getSelection()589 	public ITypedElement[] getSelection() {
590 		ArrayList<ITypedElement> result= new ArrayList<>();
591 		if (fMemberSelection != null) {
592 			Iterator<Object> iter= fArrayList.iterator();
593 			while (iter.hasNext()) {
594 				Object edition= iter.next();
595 				Object item= fMemberSelection.get(edition);
596 				if (item != null)
597 					result.add((ITypedElement) item);
598 			}
599 		} else if (fSelectedItem != null)
600 			result.add(fSelectedItem);
601 		return result.toArray(new ITypedElement[result.size()]);
602 	}
603 
604 	/**
605 	 * Returns a label for identifying the target side of a compare viewer.
606 	 * This implementation extracts the value for the key "targetLabel" from the resource bundle
607 	 * and passes it as the format argument to <code>MessageFormat.format</code>.
608 	 * The single format argument for <code>MessageFormat.format</code> ("{0}" in the format string)
609 	 * is the name of the given input element.
610 	 * <p>
611 	 * Subclasses may override to create their own label.
612 	 * </p>
613 	 *
614 	 * @param target the target element for which a label must be returned
615 	 * @param item if a path has been specified in <code>selectEdition</code> a sub element of the given target; otherwise the same as target
616 	 * @return a label the target side of a compare viewer
617 	 */
getTargetLabel(ITypedElement target, ITypedElement item)618 	protected String getTargetLabel(ITypedElement target, ITypedElement item) {
619 		String format= null;
620 		if (target instanceof ResourceNode)
621 			format= Utilities.getString(fBundle, "workspaceTargetLabel", null); //$NON-NLS-1$
622 		if (format == null)
623 			format= Utilities.getString(fBundle, "targetLabel"); //$NON-NLS-1$
624 		if (format == null)
625 			format= "x{0}"; //$NON-NLS-1$
626 
627 		return formatString(format, target.getName());
628 	}
629 
formatString(String string, String variable)630 	private String formatString(String string, String variable) {
631 		// Only process the string if it contains a variable or an escaped quote (see bug 190023)
632 		if (hasVariable(string) || hasDoubleQuotes(string))
633 			return MessageFormat.format(string, variable);
634 		return string;
635 	}
636 
hasDoubleQuotes(String string)637 	private boolean hasDoubleQuotes(String string) {
638 		return string.contains("''"); //$NON-NLS-1$
639 	}
640 
hasVariable(String string)641 	private boolean hasVariable(String string) {
642 		return string.contains("{0}"); //$NON-NLS-1$
643 	}
644 
645 	/**
646 	 * Returns a label for identifying the edition side of a compare viewer.
647 	 * This implementation extracts the value for the key "editionLabel" from the resource bundle
648 	 * and passes it as the format argument to <code>MessageFormat.format</code>.
649 	 * The single format argument for <code>MessageFormat.format</code> ("{0}" in the format string)
650 	 * is the formatted modification date of the given input element.
651 	 * <p>
652 	 * Subclasses may override to create their own label.
653 	 * </p>
654 	 *
655 	 * @param selectedEdition the selected edition for which a label must be returned
656 	 * @param item if a path has been specified in <code>selectEdition</code> a sub element of the given selectedEdition; otherwise the same as selectedEdition
657 	 * @return a label for the edition side of a compare viewer
658 	 */
getEditionLabel(ITypedElement selectedEdition, ITypedElement item)659 	protected String getEditionLabel(ITypedElement selectedEdition, ITypedElement item) {
660 		String format= null;
661 		if (selectedEdition instanceof ResourceNode)
662 			format= Utilities.getString(fBundle, "workspaceEditionLabel", null);	//$NON-NLS-1$
663 		else if (selectedEdition instanceof HistoryItem)
664 			format= Utilities.getString(fBundle, "historyEditionLabel", null);	//$NON-NLS-1$
665 		if (format == null)
666 			format= Utilities.getString(fBundle, "editionLabel");	//$NON-NLS-1$
667 		if (format == null)
668 			format= "x{0}";	//$NON-NLS-1$
669 
670 
671 		String date= "";	//$NON-NLS-1$
672 		if (selectedEdition instanceof IModificationDate) {
673 			long modDate= ((IModificationDate)selectedEdition).getModificationDate();
674 			date= DateFormat.getDateTimeInstance().format(new Date(modDate));
675 		}
676 
677 		return formatString(format, date);
678 	}
679 
680 	/**
681 	 * Returns a label for identifying a node in the edition tree viewer.
682 	 * This implementation extracts the value for the key "workspaceTreeFormat" or
683 	 * "treeFormat" (in that order) from the resource bundle
684 	 * and passes it as the format argument to <code>MessageFormat.format</code>.
685 	 * The single format argument for <code>MessageFormat.format</code> ("{0}" in the format string)
686 	 * is the formatted modification date of the given input element.
687 	 * <p>
688 	 * Subclasses may override to create their own label.
689 	 * </p>
690 	 *
691 	 * @param edition the edition for which a label must be returned
692 	 * @param item if a path has been specified in <code>edition</code> a sub element of the given edition; otherwise the same as edition
693 	 * @param date this date will be returned as part of the formatted string
694 	 * @return a label of a node in the edition tree viewer
695 	 * @since 2.0
696 	 */
getShortEditionLabel(ITypedElement edition, ITypedElement item, Date date)697 	protected String getShortEditionLabel(ITypedElement edition, ITypedElement item, Date date) {
698 		String format= null;
699 		if (edition instanceof ResourceNode)
700 			format= Utilities.getString(fBundle, "workspaceTreeFormat", null);	//$NON-NLS-1$
701 		if (format == null)
702 			format= Utilities.getString(fBundle, "treeFormat", null);	//$NON-NLS-1$
703 		if (format == null)
704 			format= "x{0}"; //$NON-NLS-1$
705 
706 		String ds= DateFormat.getTimeInstance().format(date);
707 		return formatString(format, ds);
708 	}
709 
710 	/**
711 	 * Returns an image for identifying the edition side of a compare viewer.
712 	 * This implementation extracts the value for the key "editionLabel" from the resource bundle
713 	 * and passes it as the format argument to <code>MessageFormat.format</code>.
714 	 * The single format argument for <code>MessageFormat.format</code> ("{0}" in the format string)
715 	 * is the formatted modification date of the given input element.
716 	 * <p>
717 	 * Subclasses may override to create their own label.
718 	 * </p>
719 	 *
720 	 * @param selectedEdition the selected edition for which a label must be returned
721 	 * @param item if a path has been specified in <code>selectEdition</code> a sub element of the given selectedEdition; otherwise the same as selectedEdition
722 	 * @return a label the edition side of a compare viewer
723 	 * @since 2.0
724 	 */
getEditionImage(ITypedElement selectedEdition, ITypedElement item)725 	protected Image getEditionImage(ITypedElement selectedEdition, ITypedElement item) {
726 		if (selectedEdition instanceof ResourceNode)
727 			return selectedEdition.getImage();
728 		if (selectedEdition instanceof HistoryItem) {
729 			if (fTimeImage == null) {
730 				String iconName= Utilities.getString(fBundle, "timeIcon", "obj16/resource_obj.png"); //$NON-NLS-1$ //$NON-NLS-2$
731 				ImageDescriptor id= CompareUIPlugin.getImageDescriptor(iconName);
732 				if (id != null)
733 					fTimeImage= id.createImage();
734 			}
735 			return fTimeImage;
736 		}
737 		return null;
738 	}
739 
740 	@Override
createDialogArea(Composite parent2)741 	protected synchronized Control createDialogArea(Composite parent2) {
742 
743 		Composite parent= (Composite) super.createDialogArea(parent2);
744 
745 		getShell().setText(Utilities.getString(fBundle, "title")); //$NON-NLS-1$
746 
747 		Splitter vsplitter= new Splitter(parent,  SWT.VERTICAL);
748 		vsplitter.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL
749 					| GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
750 
751 		vsplitter.addDisposeListener(
752 			e -> {
753 				if (fCompareConfiguration != null) {
754 					fCompareConfiguration.dispose();
755 					fCompareConfiguration= null;
756 				}
757 				if (fDateImage != null) {
758 					fDateImage.dispose();
759 					fDateImage= null;
760 				}
761 				if (fTimeImage != null) {
762 					fTimeImage.dispose();
763 					fTimeImage= null;
764 				}
765 			}
766 		);
767 
768 		if (fAddMode) {
769 			// we need two panes: the left for the elements, the right one for the editions
770 			Splitter hsplitter= new Splitter(vsplitter,  SWT.HORIZONTAL);
771 
772 			fMemberPane= new CompareViewerPane(hsplitter, SWT.BORDER | SWT.FLAT);
773 			fMemberPane.setText(Utilities.getString(fBundle, "memberPaneTitle")); //$NON-NLS-1$
774 
775 			int flags= SWT.H_SCROLL | SWT.V_SCROLL;
776 			if (fMultiSelect)
777 				flags|= SWT.CHECK;
778 			fMemberTable= new Table(fMemberPane, flags);
779 			fMemberTable.addSelectionListener(
780 				new SelectionAdapter() {
781 					@Override
782 					public void widgetSelected(SelectionEvent e) {
783 						if (e.detail == SWT.CHECK) {
784 							if (e.item instanceof TableItem) {
785 								TableItem ti= (TableItem) e.item;
786 								Object data= ti.getData();
787 								if (ti.getChecked())
788 									fArrayList.add(data);
789 								else
790 									fArrayList.remove(data);
791 
792 								if (fCommitButton != null)
793 									fCommitButton.setEnabled(fArrayList.size() > 0);
794 
795 								fMemberTable.setSelection(new TableItem[] { ti });
796 							}
797 						}
798 						handleMemberSelect(e.item);
799 					}
800 				}
801 			);
802 			fMemberPane.setContent(fMemberTable);
803 			fMemberTable.setFocus();
804 
805 			fEditionPane= new CompareViewerPane(hsplitter, SWT.BORDER | SWT.FLAT);
806 		} else {
807 			if (fStructureCompare) {
808 				// we need two panes: the left for the elements, the right one for the structured diff
809 				Splitter hsplitter= new Splitter(vsplitter,  SWT.HORIZONTAL);
810 
811 				fEditionPane= new CompareViewerPane(hsplitter, SWT.BORDER | SWT.FLAT);
812 				fStructuredComparePane= new CompareViewerSwitchingPane(hsplitter, SWT.BORDER | SWT.FLAT, true) {
813 					@Override
814 					protected Viewer getViewer(Viewer oldViewer, Object input) {
815 						if (input instanceof ICompareInput)
816 							return CompareUI.findStructureViewer(oldViewer, (ICompareInput)input, this, getCompareConfiguration());
817 						return null;
818 					}
819 				};
820 				fStructuredComparePane.addSelectionChangedListener(
821 					e -> feedInput2(e.getSelection())
822 				);
823 			} else {
824 				// only a single pane showing the editions
825 				fEditionPane= new CompareViewerPane(vsplitter, SWT.BORDER | SWT.FLAT);
826 			}
827 			if (fTitleArg == null)
828 				fTitleArg= fTargetPair.getItem().getName();
829 			String titleFormat= Utilities.getString(fBundle, "treeTitleFormat"); //$NON-NLS-1$
830 			String title= MessageFormat.format(titleFormat, fTitleArg );
831 			fEditionPane.setText(title);
832 			if (fTitleImage != null)
833 				fEditionPane.setImage(fTitleImage);
834 		}
835 
836 		fEditionTree= new Tree(fEditionPane, SWT.H_SCROLL | SWT.V_SCROLL);
837 		fEditionTree.addSelectionListener(
838 			new SelectionAdapter() {
839 //				public void widgetDefaultSelected(SelectionEvent e) {
840 //					handleDefaultSelected();
841 //				}
842 				@Override
843 				public void widgetSelected(SelectionEvent e) {
844 					feedInput(e.item);
845 				}
846 			}
847 		);
848 		fEditionPane.setContent(fEditionTree);
849 
850 		// now start the thread (and forget about it)
851 		if (fThread != null) {
852 			fThread.start();
853 			fThread= null;
854 		}
855 
856 		fContentPane= new CompareViewerSwitchingPane(vsplitter, SWT.BORDER | SWT.FLAT) {
857 			@Override
858 			protected Viewer getViewer(Viewer oldViewer, Object input) {
859 				return CompareUI.findContentViewer(oldViewer, input, this, getCompareConfiguration());
860 			}
861 		};
862 		vsplitter.setWeights(new int[] { 30, 70 });
863 
864 		statusLabel = new Label(parent, SWT.NONE);
865 		statusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
866 
867 		applyDialogFont(parent);
868 		return parent;
869 	}
870 
871 	@Override
createButtonsForButtonBar(Composite parent)872 	protected void createButtonsForButtonBar(Composite parent) {
873 		String buttonLabel= Utilities.getString(fBundle, "buttonLabel", IDialogConstants.OK_LABEL); //$NON-NLS-1$
874 		if (fCompareMode) {
875 			// only a 'Done' button
876 			createButton(parent, IDialogConstants.CANCEL_ID, buttonLabel, false);
877 		} else {
878 			// a 'Cancel' and a 'Add/Replace' button
879 			fCommitButton= createButton(parent, IDialogConstants.OK_ID, buttonLabel, true);
880 			fCommitButton.setEnabled(false);
881 			createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
882 		}
883 	}
884 
885 	/**
886 	 * Overidden to disable dismiss on double click in compare mode.
887 	 * @since 2.0
888 	 */
889 	@Override
okPressed()890 	protected void okPressed() {
891 		if (fCompareMode) {
892 			// don't dismiss dialog
893 		} else
894 			super.okPressed();
895 	}
896 
897 	//---- private stuff ----------------------------------------------------------------------------------------
898 
899 	/*
900 	 * Asynchroneously sends a Pair (or null) to the UI thread.
901 	 */
sendPair(final Pair pair)902 	private void sendPair(final Pair pair) {
903 		if (fEditionTree != null && !fEditionTree.isDisposed()) {
904 			Display display= fEditionTree.getDisplay();
905 			display.asyncExec(
906 				() -> addMemberEdition(pair)
907 			);
908 		}
909 	}
910 
internalSort(IModificationDate[] keys)911 	private static void internalSort(IModificationDate[] keys) {
912 		Arrays.sort(keys, (d1, d2) -> {
913 			long d= d2.getModificationDate() - d1.getModificationDate();
914 			if (d < 0)
915 				return -1;
916 			if (d > 0)
917 				return 1;
918 			return 0;
919 		});
920 	}
921 
922 	/*
923 	 * Adds the given Pair to the member editions.
924 	 * If HIDE_IDENTICAL is true the new Pair is only added if its contents
925 	 * is different from the preceeding Pair.
926 	 * If the argument is <code>null</code> the message "No Editions found" is shown
927 	 * in the member or edition viewer.
928 	 */
addMemberEdition(Pair pair)929 	private void addMemberEdition(Pair pair) {
930 
931 		if (pair == null) {	// end of list of pairs
932 			if (fMemberTable != null) {
933 				if (!fMemberTable.isDisposed() && fMemberTable.getItemCount() == 0) {
934 					if (fMultiSelect) {
935 						fMemberTable.dispose();
936 						fMemberTable= new Table(fMemberPane, SWT.NONE);
937 						fMemberPane.setContent(fMemberTable);
938 					}
939 					TableItem ti= new TableItem(fMemberTable, SWT.NONE);
940 					ti.setText(Utilities.getString(fBundle, "noAdditionalMembersMessage")); //$NON-NLS-1$
941 				}
942 				return;
943 			}
944 			if (fEditionTree != null && !fEditionTree.isDisposed() && fEditionTree.getItemCount() == 0) {
945 				TreeItem ti= new TreeItem(fEditionTree, SWT.NONE);
946 				ti.setText(Utilities.getString(fBundle, "notFoundInLocalHistoryMessage")); //$NON-NLS-1$
947 			}
948 			return;
949 		}
950 
951 		if (fMemberEditions == null)
952 			fMemberEditions= new HashMap<>();
953 		if (fMultiSelect && fMemberSelection == null)
954 			fMemberSelection= new HashMap<>();
955 
956 		ITypedElement item= pair.getItem();
957 		List<Pair> editions= fMemberEditions.get(item);
958 		if (editions == null) {
959 			editions= new ArrayList<>();
960 			fMemberEditions.put(item, editions);
961 			if (fMemberTable != null && !fMemberTable.isDisposed()) {
962 				ITypedElement te= item;
963 				String name= te.getName();
964 
965 				// find position
966 				TableItem[] items= fMemberTable.getItems();
967 				int where= items.length;
968 				for (int i= 0; i < where; i++) {
969 					String n= items[i].getText();
970 					if (n.compareTo(name) > 0) {
971 						where= i;
972 						break;
973 					}
974 				}
975 
976 				TableItem ti= new TableItem(fMemberTable, where, SWT.NULL);
977 				ti.setImage(te.getImage());
978 				ti.setText(name);
979 				ti.setData(editions);
980 			}
981 		}
982 		if (fHideIdentical) {
983 			Pair last= fTargetPair;
984 			int size= editions.size();
985 			if (size > 0)
986 				last= editions.get(size-1);
987 			if (last != null && last.equals(pair))
988 				return;	// don't add since the new one is equal to old
989 		}
990 		editions.add(pair);
991 
992 		if (!fAddMode || editions == fCurrentEditions)
993 			addEdition(pair);
994 	}
995 
996 	/*
997 	 * Returns the number of s since Jan 1st, 1970.
998 	 * The given date is converted to GMT and daylight saving is taken into account too.
999 	 */
dayNumber(long date)1000 	private long dayNumber(long date) {
1001 		int ONE_DAY_MS= 24*60*60 * 1000; // one day in milli seconds
1002 
1003 		Calendar calendar= Calendar.getInstance();
1004 		long localTimeOffset= calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
1005 
1006 		return (date + localTimeOffset) / ONE_DAY_MS;
1007 	}
1008 
1009 	/*
1010 	 * Adds the given Pair to the edition tree.
1011 	 * It takes care of creating tree nodes for different dates.
1012 	 */
addEdition(Pair pair)1013 	private void addEdition(Pair pair) {
1014 		if (fEditionTree == null || fEditionTree.isDisposed())
1015 			return;
1016 
1017 		// find last day
1018 		TreeItem[] days= fEditionTree.getItems();
1019 		TreeItem lastDay= null;
1020 		if (days.length > 0)
1021 			lastDay= days[days.length-1];
1022 
1023 		boolean first= lastDay == null;
1024 
1025 		ITypedElement edition= pair.getEdition();
1026 		ITypedElement item= pair.getItem();
1027 
1028 		long ldate= ((IModificationDate)edition).getModificationDate();
1029 		long day= dayNumber(ldate);
1030 		Date date= new Date(ldate);
1031 		if (lastDay == null || day != dayNumber(((Date)lastDay.getData()).getTime())) {
1032 			lastDay= new TreeItem(fEditionTree, SWT.NONE);
1033 			if (fDateImage == null) {
1034 				String iconName= Utilities.getString(fBundle, "dateIcon", "obj16/day_obj.png"); //$NON-NLS-2$ //$NON-NLS-1$
1035 				ImageDescriptor id= CompareUIPlugin.getImageDescriptor(iconName);
1036 				if (id != null)
1037 					fDateImage= id.createImage();
1038 			}
1039 			lastDay.setImage(fDateImage);
1040 			String df= DateFormat.getDateInstance().format(date);
1041 			long today= dayNumber(System.currentTimeMillis());
1042 
1043 			String formatKey;
1044 			if (day == today)
1045 				formatKey= "todayFormat"; //$NON-NLS-1$
1046 			else if (day == today-1)
1047 				formatKey= "yesterdayFormat"; //$NON-NLS-1$
1048 			else
1049 				formatKey= "dayFormat"; //$NON-NLS-1$
1050 			String pattern= Utilities.getString(fBundle, formatKey);
1051 			if (pattern != null)
1052 				df= MessageFormat.format(pattern, df);
1053 			lastDay.setText(df);
1054 			lastDay.setData(date);
1055 		}
1056 		TreeItem ti= new TreeItem(lastDay, SWT.NONE);
1057 		ti.setImage(getEditionImage(edition, item));
1058 
1059 		String s= getShortEditionLabel(edition, item, date);
1060 		if (pair.fHasError) {
1061 			String pattern= Utilities.getString(fBundle, "parseErrorFormat"); //$NON-NLS-1$
1062 			s= MessageFormat.format(pattern, s);
1063 		}
1064 		ti.setText(s);
1065 
1066 		ti.setData(pair);
1067 
1068 		// determine selected TreeItem
1069 		TreeItem selection= first ? ti : null;
1070 		if (fMemberSelection != null) {
1071 			Object selected= fMemberSelection.get(fCurrentEditions);
1072 			if (selected != null) {
1073 				if (selected == pair.getItem())
1074 					selection= ti;
1075 				else
1076 					selection= null;
1077 			}
1078 		}
1079 		if (selection != null) {
1080 			fEditionTree.setSelection(new TreeItem[] { selection });
1081 			if (!fAddMode)
1082 				fEditionTree.setFocus();
1083 			feedInput(selection);
1084 		}
1085 
1086 		if (first) // expand first node
1087 			lastDay.setExpanded(true);
1088 	}
1089 
1090 	/*
1091 	 * Feeds selection from member viewer to edition viewer.
1092 	 */
handleMemberSelect(Widget w)1093 	private void handleMemberSelect(Widget w) {
1094 		Object data= w.getData();
1095 		if (data instanceof List) {
1096 			@SuppressWarnings("unchecked")
1097 			List<Object> editions= (List<Object>) data;
1098 			if (editions != fCurrentEditions) {
1099 				fCurrentEditions= editions;
1100 				fEditionTree.removeAll();
1101 
1102 				String pattern= Utilities.getString(fBundle, "treeTitleFormat"); //$NON-NLS-1$
1103 				String title= MessageFormat.format(pattern, new Object[] { ((Item)w).getText() });
1104 				fEditionPane.setText(title);
1105 
1106 				Iterator<Object> iter= editions.iterator();
1107 				while (iter.hasNext()) {
1108 					Object item= iter.next();
1109 					if (item instanceof Pair)
1110 						addEdition((Pair) item);
1111 				}
1112 			}
1113 		}
1114 	}
1115 
setInput(Object input)1116 	private void setInput(Object input) {
1117 		if (!fCompare && input instanceof ICompareInput) {
1118 			ICompareInput ci= (ICompareInput) input;
1119 			if (fTargetIsRight)
1120 				input= ci.getLeft();
1121 			else
1122 				input= ci.getRight();
1123 		}
1124 		fContentPane.setInput(input);
1125 		if (fStructuredComparePane != null)
1126 			fStructuredComparePane.setInput(input);
1127 	}
1128 
1129 	/*
1130 	 * Feeds selection from edition viewer to content (and structure) viewer.
1131 	 */
feedInput(Widget w)1132 	private void feedInput(Widget w) {
1133 		Object input= w.getData();
1134 		boolean isOK= false;
1135 		if (input instanceof Pair) {
1136 			Pair pair= (Pair) input;
1137 			fSelectedItem= pair.getItem();
1138 			isOK= !pair.fHasError;
1139 
1140 			ITypedElement edition= pair.getEdition();
1141 			String editionLabel= getEditionLabel(edition, fSelectedItem);
1142 			Image editionImage= getEditionImage(edition, fSelectedItem);
1143 
1144 			if (fAddMode) {
1145 				if (fMemberSelection != null)
1146 					fMemberSelection.put(fCurrentEditions, fSelectedItem);
1147 				setInput(fSelectedItem);
1148 				fContentPane.setText(editionLabel);
1149 				fContentPane.setImage(editionImage);
1150 			} else {
1151 				getCompareConfiguration();
1152 				if (fTargetIsRight) {
1153 					fCompareConfiguration.setLeftLabel(editionLabel);
1154 					fCompareConfiguration.setLeftImage(editionImage);
1155 					setInput(new DiffNode(fSelectedItem, fTargetPair.getItem()));
1156 				} else {
1157 					fCompareConfiguration.setRightLabel(editionLabel);
1158 					fCompareConfiguration.setRightImage(editionImage);
1159 					setInput(new DiffNode(fTargetPair.getItem(), fSelectedItem));
1160 				}
1161 			}
1162 		} else {
1163 			fSelectedItem= null;
1164 			setInput(null);
1165 		}
1166 		if (fCommitButton != null) {
1167 			if (fMultiSelect)
1168 				fCommitButton.setEnabled(isOK && fSelectedItem != null && fArrayList.size() > 0);
1169 			else
1170 				fCommitButton.setEnabled(isOK && fSelectedItem != null && fTargetPair.getItem() != fSelectedItem);
1171 		}
1172 	}
1173 
1174 	/*
1175 	 * Feeds selection from structure viewer to content viewer.
1176 	 */
feedInput2(ISelection sel)1177 	private void feedInput2(ISelection sel) {
1178 		if (sel instanceof IStructuredSelection) {
1179 			IStructuredSelection ss= (IStructuredSelection) sel;
1180 			if (ss.size() == 1)
1181 				fContentPane.setInput(ss.getFirstElement());
1182 		}
1183 	}
1184 }
1185