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