1 /*******************************************************************************
2  * Copyright (c) 2006, 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.structuremergeviewer;
15 
16 import java.io.BufferedReader;
17 import java.io.StringReader;
18 import java.io.UnsupportedEncodingException;
19 import java.util.List;
20 
21 import org.eclipse.compare.CompareUI;
22 import org.eclipse.compare.ICompareFilter;
23 import org.eclipse.compare.IEditableContent;
24 import org.eclipse.compare.IEncodedStreamContentAccessor;
25 import org.eclipse.compare.ISharedDocumentAdapter;
26 import org.eclipse.compare.IStreamContentAccessor;
27 import org.eclipse.compare.ITypedElement;
28 import org.eclipse.compare.SharedDocumentAdapter;
29 import org.eclipse.compare.contentmergeviewer.IDocumentRange;
30 import org.eclipse.compare.internal.CompareUIPlugin;
31 import org.eclipse.compare.internal.Utilities;
32 import org.eclipse.compare.internal.patch.LineReader;
33 import org.eclipse.core.resources.ResourcesPlugin;
34 import org.eclipse.core.runtime.CoreException;
35 import org.eclipse.core.runtime.IProgressMonitor;
36 import org.eclipse.core.runtime.OperationCanceledException;
37 import org.eclipse.jface.text.Document;
38 import org.eclipse.jface.text.IDocument;
39 import org.eclipse.jface.text.IDocumentExtension3;
40 import org.eclipse.jface.text.IDocumentPartitioner;
41 import org.eclipse.ui.IEditorInput;
42 import org.eclipse.ui.services.IDisposable;
43 import org.eclipse.ui.texteditor.IDocumentProvider;
44 
45 /**
46  * An {@link IStructureCreator2} that attempts to use an {@link IDocumentProvider}
47  * to obtain a shared document for an {@link ITypedElement}.
48  * <p>
49  * Clients may subclass this class.
50  * </p>
51  *
52  * @since 3.3
53  */
54 public abstract class StructureCreator implements IStructureCreator2 {
55 	@Override
getStructure(Object input)56 	public IStructureComparator getStructure(Object input) {
57 		String contents= null;
58 		IDocument doc= CompareUI.getDocument(input);
59 		if (doc == null) {
60 			if (input instanceof IStreamContentAccessor) {
61 				IStreamContentAccessor sca= (IStreamContentAccessor) input;
62 				try {
63 					contents= Utilities.readString(sca);
64 				} catch (CoreException e) {
65 					// return null indicates the error.
66 					CompareUIPlugin.log(e);
67 					return null;
68 				}
69 			}
70 
71 			if (contents == null) {
72 				// Node has no contents
73 				return null;
74 			}
75 
76 			doc= new Document(contents);
77 			setupDocument(doc);
78 		}
79 
80 		try {
81 			return createStructureComparator(input, doc, null, null);
82 		} catch (CoreException e) {
83 			CompareUIPlugin.log(e);
84 			return null;
85 		}
86 	}
87 
88 	@Override
createStructure(final Object element, final IProgressMonitor monitor)89 	public IStructureComparator createStructure(final Object element,
90 			final IProgressMonitor monitor) throws CoreException {
91 		final IStructureComparator[] result = new IStructureComparator[] { null };
92 		Runnable runnable = () -> {
93 			try {
94 				result[0]= internalCreateStructure(element, monitor);
95 			} catch (OperationCanceledException ex) {
96 				return;
97 			}
98 		};
99 		Utilities.runInUIThread(runnable);
100 		return result[0];
101 	}
102 
103 	/*
104 	 * We need to create the structure in the UI thread since IDocument requires this
105 	 */
internalCreateStructure(Object element, IProgressMonitor monitor)106 	private IStructureComparator internalCreateStructure(Object element,
107 			IProgressMonitor monitor) {
108 		final ISharedDocumentAdapter sda = SharedDocumentAdapterWrapper.getAdapter(element);
109 		if (sda != null) {
110 			final IEditorInput input = sda.getDocumentKey(element);
111 			if (input != null) {
112 				final IDocumentProvider provider = SharedDocumentAdapter.getDocumentProvider(input);
113 				if (provider != null) {
114 					try {
115 						sda.connect(provider, input);
116 						IDocument document = provider.getDocument(input);
117 						setupDocument(document);
118 						return createStructureComparator(element, document, wrapSharedDocumentAdapter(sda, element, document), monitor);
119 					} catch (CoreException e) {
120 						// Connection to the document provider failed.
121 						// Log and fall through to use simple structure
122 						CompareUIPlugin.log(e);
123 					}
124 				}
125 			}
126 		}
127 		return getStructure(element);
128 	}
129 
130 	/**
131 	 * Creates an {@link IStructureComparator} for the given element using the
132 	 * contents available in the given document. If the provided
133 	 * {@link ISharedDocumentAdapter} is not <code>null</code> then the
134 	 * {@link IStructureComparator} returned by this method must implement the
135 	 * {@link IDisposable} interface and disconnect from the adapter when the
136 	 * comparator is disposed. The {@link StructureDiffViewer} class will call
137 	 * dispose if the {@link IStructureComparator} also implements
138 	 * {@link IDisposable}. Other clients must do the same.
139 	 * <p>
140 	 * It should be noted that the provided {@link ISharedDocumentAdapter}
141 	 * will provide the key associated with the given element when
142 	 * {@link ISharedDocumentAdapter#getDocumentKey(Object)} is called
143 	 * for any {@link IDocumentRange} node whose document matches the
144 	 * provided document. Thus, this adapter should also be returned
145 	 * by the structure comparator and its children when they are adapted
146 	 * to an {@link ISharedDocumentAdapter}.
147 	 * @param element the element
148 	 * @param document the document that has the contents for the element
149 	 * @param sharedDocumentAdapter the shared document adapter from which the
150 	 *            document was obtained or <code>null</code> if the document
151 	 *            is not shared.
152 	 * @param monitor a progress monitor or <code>null</code> if progress is not required
153 	 *
154 	 * @return a structure comparator
155 	 * @throws CoreException if creating the comparator failed; depends on actual implementation
156 	 */
createStructureComparator( final Object element, IDocument document, final ISharedDocumentAdapter sharedDocumentAdapter, IProgressMonitor monitor)157 	protected abstract IStructureComparator createStructureComparator(
158 			final Object element, IDocument document,
159 			final ISharedDocumentAdapter sharedDocumentAdapter,
160 			IProgressMonitor monitor) throws CoreException;
161 
162 	/**
163 	 * Sets up the newly created document as appropriate. Any document partitioners
164 	 * should be added to a custom slot using the {@link IDocumentExtension3} interface
165 	 * in case the document is shared via a file buffer.
166 	 * @param document a document
167 	 */
setupDocument(IDocument document)168 	protected void setupDocument(IDocument document) {
169 		String partitioning = getDocumentPartitioning();
170 		if (partitioning == null || !(document instanceof IDocumentExtension3)) {
171 			if (document.getDocumentPartitioner() == null) {
172 				IDocumentPartitioner partitioner= getDocumentPartitioner();
173 				if (partitioner != null) {
174 					document.setDocumentPartitioner(partitioner);
175 					partitioner.connect(document);
176 				}
177 			}
178 		} else {
179 			IDocumentExtension3 ex3 = (IDocumentExtension3) document;
180 			if (ex3.getDocumentPartitioner(partitioning) == null) {
181 				IDocumentPartitioner partitioner= getDocumentPartitioner();
182 				if (partitioner != null) {
183 					ex3.setDocumentPartitioner(partitioning, partitioner);
184 					partitioner.connect(document);
185 				}
186 			}
187 		}
188 	}
189 
190 	/**
191 	 * Returns the partitioner to be associated with the document or
192 	 * <code>null</code> is partitioning is not needed or if the subclass
193 	 * overrode {@link #setupDocument(IDocument)} directly.
194 	 * @return a partitioner
195 	 */
getDocumentPartitioner()196 	protected IDocumentPartitioner getDocumentPartitioner() {
197 		return null;
198 	}
199 
200 	/**
201 	 * Returns the partitioning to which the partitioner returned from
202 	 * {@link #getDocumentPartitioner()} is to be associated. Return <code>null</code>
203 	 * only if partitioning is not needed or if the subclass
204 	 * overrode {@link #setupDocument(IDocument)} directly.
205 	 * @see IDocumentExtension3
206 	 * @return a partitioning
207 	 */
getDocumentPartitioning()208 	protected String getDocumentPartitioning() {
209 		return null;
210 	}
211 
212 	/**
213 	 * Default implementation of save that extracts the contents from
214 	 * the document of an {@link IDocumentRange} and sets it on the
215 	 * input. If the input is an {@link IEncodedStreamContentAccessor},
216 	 * the charset of the input is used to extract the contents from the
217 	 * document. If the input adapts to {@link ISharedDocumentAdapter} and
218 	 * the document of the {@link IDocumentRange} matches that of the
219 	 * input, then the save is issued through the shared document adapter.
220 	 * @see org.eclipse.compare.structuremergeviewer.IStructureCreator#save(org.eclipse.compare.structuremergeviewer.IStructureComparator, java.lang.Object)
221 	 */
222 	@Override
save(IStructureComparator node, Object input)223 	public void save(IStructureComparator node, Object input) {
224 		if (node instanceof IDocumentRange && input instanceof IEditableContent) {
225 			IDocument document= ((IDocumentRange)node).getDocument();
226 			// First check to see if we have a shared document
227 			final ISharedDocumentAdapter sda = SharedDocumentAdapterWrapper.getAdapter(input);
228 			if (sda != null) {
229 				IEditorInput key = sda.getDocumentKey(input);
230 				if (key != null) {
231 					IDocumentProvider provider = SharedDocumentAdapter.getDocumentProvider(key);
232 					if (provider != null) {
233 						IDocument providerDoc = provider.getDocument(key);
234 						// We have to make sure that the document we are saving is the same as the shared document
235 						if (providerDoc != null && providerDoc == document) {
236 							if (save(provider, document, input, sda, key))
237 								return;
238 						}
239 					}
240 				}
241 			}
242 			IEditableContent bca= (IEditableContent) input;
243 			String contents= document.get();
244 			String encoding= null;
245 			if (input instanceof IEncodedStreamContentAccessor) {
246 				try {
247 					encoding= ((IEncodedStreamContentAccessor)input).getCharset();
248 				} catch (CoreException e1) {
249 					// ignore
250 				}
251 			}
252 			if (encoding == null)
253 				encoding= ResourcesPlugin.getEncoding();
254 			byte[] bytes;
255 			try {
256 				bytes= contents.getBytes(encoding);
257 			} catch (UnsupportedEncodingException e) {
258 				bytes= contents.getBytes();
259 			}
260 			bca.setContent(bytes);
261 		}
262 	}
263 
save(final IDocumentProvider provider, final IDocument document, final Object input, final ISharedDocumentAdapter sda, final IEditorInput key)264 	private boolean save(final IDocumentProvider provider, final IDocument document,
265 			final Object input, final ISharedDocumentAdapter sda, final IEditorInput key) {
266 		try {
267 			sda.flushDocument(provider, key, document, false);
268 			return true;
269 		} catch (CoreException e) {
270 			CompareUIPlugin.log(e);
271 		}
272 		return false;
273 	}
274 
275 	/**
276 	 * Create an {@link ISharedDocumentAdapter} that will provide the document key for the given input
277 	 * object for any {@link DocumentRangeNode} instances whose document is the same as the
278 	 * provided document.
279 	 * @param input the input element
280 	 * @param document the document associated with the input element
281 	 * @return a shared document adapter that provides the proper document key for document range nodes
282 	 */
wrapSharedDocumentAdapter(ISharedDocumentAdapter elementAdapter, final Object input, final IDocument document)283 	private final ISharedDocumentAdapter wrapSharedDocumentAdapter(ISharedDocumentAdapter elementAdapter, final Object input, final IDocument document) {
284 		// We need to wrap the adapter so that the proper document key gets returned
285 		return new SharedDocumentAdapterWrapper(elementAdapter) {
286 			@Override
287 			public IEditorInput getDocumentKey(Object element) {
288 				if (hasSameDocument(element)) {
289 					return super.getDocumentKey(input);
290 				}
291 				return super.getDocumentKey(element);
292 			}
293 			private boolean hasSameDocument(Object element) {
294 				if (element instanceof DocumentRangeNode) {
295 					DocumentRangeNode drn = (DocumentRangeNode) element;
296 					return drn.getDocument() == document;
297 				}
298 				return false;
299 			}
300 		};
301 	}
302 
303 	/**
304 	 * Default implementation of {@link #createElement(Object, Object, IProgressMonitor)}
305 	 * that uses {@link #getPath(Object, Object)} to determine the
306 	 * path for the element, {@link #createStructure(Object, IProgressMonitor)} to create the structure
307 	 * and {@link #findElement(IStructureComparator, String[])} to find the
308 	 * element in the structure. Subclasses may override.
309 	 * @param element the element
310 	 * @param input the containing input
311 	 * @param monitor a progress monitor
312 	 * @return the sub-structure element in the input for the given element
313 	 * @throws CoreException if a parse error occurred
314 	 */
315 	@Override
316 	public ITypedElement createElement(Object element, Object input, IProgressMonitor monitor)
317 			throws CoreException {
318 		String[] path= getPath(element, input);
319 		if (path == null) {
320 			// TODO: Temporary code until subclasses are updated
321 			IStructureComparator locate = locate(element, input);
322 			if (locate instanceof ITypedElement) {
323 				return (ITypedElement)locate;
324 			}
325 			return null;
326 		}
327 
328 		// Build the structure
329 		IStructureComparator structure= createStructure(input, monitor);
330 		if (structure == null)	// we couldn't parse the structure
331 			return null;		// so we can't find anything
332 
333 		// find the path in the tree
334 		return findElement(structure, path);
335 	}
336 
337 	/**
338 	 * Default implementation of {@link #locate(Object, Object)} that
339 	 * uses {@link #getPath(Object, Object)} to determine the
340 	 * path for the element, {@link #getStructure(Object)} to create the structure
341 	 * and {@link #findElement(IStructureComparator, String[])} to find the
342 	 * element in the structure.  Subclasses may override.
343 	 * @param element the element
344 	 * @param input the containing input
345 	 * @return the sub-structure element in the input for the given element
346 	 */
347 	@Override
348 	public IStructureComparator locate(Object element, Object input) {
349 		String[] path= getPath(element, input);
350 		if (path == null)
351 			return null;
352 		// Build the structure
353 		IStructureComparator structure= getStructure(input);
354 		if (structure == null)	// we couldn't parse the structure
355 			return null;		// so we can't find anything
356 
357 		// find the path in the tree
358 		return (IStructureComparator)findElement(structure, path);
359 	}
360 
361 	/**
362 	 * Finds the element at the given path in the given structure.
363 	 * This method is invoked from the {@link #createElement(Object, Object, IProgressMonitor)}
364 	 * and {@link #locate(Object, Object)} methods to find the element for
365 	 * the given path.
366 	 * @param structure the structure
367 	 * @param path the path of an element in the structure
368 	 * @return the element at the given path in the structure or <code>null</code>
369 	 */
370 	protected ITypedElement findElement(IStructureComparator structure, String[] path) {
371 		return (ITypedElement)find(structure, path, 0);
372 	}
373 
374 	/**
375 	 * Recursively extracts the given path from the tree.
376 	 */
377 	private IStructureComparator find(IStructureComparator tree, String[] path, int index) {
378 		if (tree != null) {
379 			Object[] children= tree.getChildren();
380 			if (children != null) {
381 				for (Object c : children) {
382 					IStructureComparator child = (IStructureComparator) c;
383 					if (child instanceof ITypedElement && child instanceof DocumentRangeNode) {
384 						String n1= ((DocumentRangeNode)child).getId();
385 						if (n1 == null) {
386 							n1= ((ITypedElement)child).getName();
387 						}
388 						String n2= path[index];
389 						if (n1.equals(n2)) {
390 							if (index == path.length-1)
391 								return child;
392 							IStructureComparator result= find(child, path, index+1);
393 							if (result != null)
394 								return result;
395 						}
396 					}
397 				}
398 			}
399 		}
400 		return null;
401 	}
402 
403 	/**
404 	 * Returns the path of the element in the structure of it's containing input
405 	 * or <code>null</code> if the element is not contained in the input. This method is
406 	 * invoked from {@link #createElement(Object, Object, IProgressMonitor)} and
407 	 * {@link #locate(Object, Object)} methods to determine
408 	 * the path to be passed to {@link #findElement(IStructureComparator, String[])}.
409 	 * By default, <code>null</code> is returned. Subclasses may override.
410 	 * @param element the element
411 	 * @param input the input
412 	 * @return the path of the element in the structure of it's containing input
413 	 * or <code>null</code>
414 	 */
415 	protected String[] getPath(Object element, Object input) {
416 		return null;
417 	}
418 
419 	@Override
420 	public void destroy(Object object) {
421 		IDisposable disposable = getDisposable(object);
422 		if (disposable != null)
423 			disposable.dispose();
424 	}
425 
426 	private IDisposable getDisposable(Object object) {
427 		if (object instanceof IDisposable) {
428 			return (IDisposable) object;
429 		}
430 		if (object instanceof DocumentRangeNode) {
431 			DocumentRangeNode node = (DocumentRangeNode) object;
432 			return getDisposable(node.getParentNode());
433 		}
434 		return null;
435 	}
436 
437 	/**
438 	 * Returns true if the two nodes are equal for comparison purposes. If
439 	 * <code>compareFilters</code> is not empty, the filters are applied to each
440 	 * line of each node's text representation.
441 	 *
442 	 * @param node1            first node
443 	 * @param contributor1     either 'A', 'L', or 'R' for ancestor, left or right
444 	 *                         contributor
445 	 * @param node2            second node
446 	 * @param contributor2     either 'A', 'L', or 'R' for ancestor, left or right
447 	 *                         contributor
448 	 * @param ignoreWhitespace if <code>true</code> whitespace characters will be
449 	 *                         ignored when determining equality. Note: Will bypass
450 	 *                         any custom ignore whitespace behaviors contributed
451 	 *                         through implementations of
452 	 *                         <code>org.eclipse.compare.structuremergeviewer.IStructureCreator.getContents()</code>
453 	 * @param compareFilters   the filters used to customize the comparison of lines
454 	 *                         of text.
455 	 * @return whether the two nodes are equal for comparison purposes
456 	 * @noreference This method is not intended to be referenced by clients.
457 	 * @since 3.6
458 	 */
459 	public boolean contentsEquals(Object node1, char contributor1,
460 			Object node2, char contributor2, boolean ignoreWhitespace,
461 			ICompareFilter[] compareFilters) {
462 
463 		List<String> lines1 = LineReader.readLines(new BufferedReader(new StringReader(
464 				getContents(node1, false))));
465 		List<String> lines2 = LineReader.readLines(new BufferedReader(new StringReader(
466 				getContents(node2, false))));
467 
468 		StringBuilder buffer1 = new StringBuilder();
469 		StringBuilder buffer2 = new StringBuilder();
470 
471 		int maxLines = Math.max(lines1.size(), lines2.size());
472 		for (int i = 0; i < maxLines; i++) {
473 			String s1 = lines1.size() > i ? (String) lines1.get(i) : ""; //$NON-NLS-1$
474 			String s2 = lines2.size() > i ? (String) lines2.get(i) : ""; //$NON-NLS-1$
475 
476 			if (compareFilters != null && compareFilters.length > 0) {
477 				s1 = Utilities.applyCompareFilters(s1, contributor1, s2,
478 						contributor2, compareFilters);
479 				s2 = Utilities.applyCompareFilters(s2, contributor2, s1,
480 						contributor1, compareFilters);
481 			}
482 			buffer1.append(s1);
483 			buffer2.append(s2);
484 		}
485 		if (ignoreWhitespace) {
486 			int l1 = buffer1.length();
487 			int l2 = buffer2.length();
488 			int c1 = 0, c2 = 0;
489 			int i1 = 0, i2 = 0;
490 
491 			while (c1 != -1) {
492 
493 				c1 = -1;
494 				while (i1 < l1) {
495 					char c = buffer1.charAt(i1++);
496 					if (!Character.isWhitespace(c)) {
497 						c1 = c;
498 						break;
499 					}
500 				}
501 
502 				c2 = -1;
503 				while (i2 < l2) {
504 					char c = buffer2.charAt(i2++);
505 					if (!Character.isWhitespace(c)) {
506 						c2 = c;
507 						break;
508 					}
509 				}
510 
511 				if (c1 != c2)
512 					return false;
513 			}
514 		} else if (!buffer1.toString().equals(buffer2.toString())) {
515 			return false;
516 		}
517 
518 		return true;
519 	}
520 }
521