| /******************************************************************************* |
| * Copyright (c) 2006, 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 |
| *******************************************************************************/ |
| package org.eclipse.compare.structuremergeviewer; |
| |
| import java.io.BufferedReader; |
| import java.io.StringReader; |
| import java.io.UnsupportedEncodingException; |
| import java.util.List; |
| |
| import org.eclipse.compare.CompareUI; |
| import org.eclipse.compare.ICompareFilter; |
| import org.eclipse.compare.IEditableContent; |
| import org.eclipse.compare.IEncodedStreamContentAccessor; |
| import org.eclipse.compare.ISharedDocumentAdapter; |
| import org.eclipse.compare.IStreamContentAccessor; |
| import org.eclipse.compare.ITypedElement; |
| import org.eclipse.compare.SharedDocumentAdapter; |
| import org.eclipse.compare.contentmergeviewer.IDocumentRange; |
| import org.eclipse.compare.internal.CompareUIPlugin; |
| import org.eclipse.compare.internal.Utilities; |
| import org.eclipse.compare.internal.patch.LineReader; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.jface.text.Document; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentExtension3; |
| import org.eclipse.jface.text.IDocumentPartitioner; |
| import org.eclipse.ui.IEditorInput; |
| import org.eclipse.ui.services.IDisposable; |
| import org.eclipse.ui.texteditor.IDocumentProvider; |
| |
| /** |
| * An {@link IStructureCreator2} that attempts to use an {@link IDocumentProvider} |
| * to obtain a shared document for an {@link ITypedElement}. |
| * <p> |
| * Clients may subclass this class. |
| * </p> |
| * |
| * @since 3.3 |
| */ |
| public abstract class StructureCreator implements IStructureCreator2 { |
| @Override |
| public IStructureComparator getStructure(Object input) { |
| String contents= null; |
| IDocument doc= CompareUI.getDocument(input); |
| if (doc == null) { |
| if (input instanceof IStreamContentAccessor) { |
| IStreamContentAccessor sca= (IStreamContentAccessor) input; |
| try { |
| contents= Utilities.readString(sca); |
| } catch (CoreException e) { |
| // return null indicates the error. |
| CompareUIPlugin.log(e); |
| return null; |
| } |
| } |
| |
| if (contents == null) { |
| // Node has no contents |
| return null; |
| } |
| |
| doc= new Document(contents); |
| setupDocument(doc); |
| } |
| |
| try { |
| return createStructureComparator(input, doc, null, null); |
| } catch (CoreException e) { |
| CompareUIPlugin.log(e); |
| return null; |
| } |
| } |
| |
| @Override |
| public IStructureComparator createStructure(final Object element, |
| final IProgressMonitor monitor) throws CoreException { |
| final IStructureComparator[] result = new IStructureComparator[] { null }; |
| Runnable runnable = () -> { |
| try { |
| result[0]= internalCreateStructure(element, monitor); |
| } catch (OperationCanceledException ex) { |
| return; |
| } |
| }; |
| Utilities.runInUIThread(runnable); |
| return result[0]; |
| } |
| |
| /* |
| * We need to create the structure in the UI thread since IDocument requires this |
| */ |
| private IStructureComparator internalCreateStructure(Object element, |
| IProgressMonitor monitor) { |
| final ISharedDocumentAdapter sda = SharedDocumentAdapterWrapper.getAdapter(element); |
| if (sda != null) { |
| final IEditorInput input = sda.getDocumentKey(element); |
| if (input != null) { |
| final IDocumentProvider provider = SharedDocumentAdapter.getDocumentProvider(input); |
| if (provider != null) { |
| try { |
| sda.connect(provider, input); |
| IDocument document = provider.getDocument(input); |
| setupDocument(document); |
| return createStructureComparator(element, document, wrapSharedDocumentAdapter(sda, element, document), monitor); |
| } catch (CoreException e) { |
| // Connection to the document provider failed. |
| // Log and fall through to use simple structure |
| CompareUIPlugin.log(e); |
| } |
| } |
| } |
| } |
| return getStructure(element); |
| } |
| |
| /** |
| * Creates an {@link IStructureComparator} for the given element using the |
| * contents available in the given document. If the provided |
| * {@link ISharedDocumentAdapter} is not <code>null</code> then the |
| * {@link IStructureComparator} returned by this method must implement the |
| * {@link IDisposable} interface and disconnect from the adapter when the |
| * comparator is disposed. The {@link StructureDiffViewer} class will call |
| * dispose if the {@link IStructureComparator} also implements |
| * {@link IDisposable}. Other clients must do the same. |
| * <p> |
| * It should be noted that the provided {@link ISharedDocumentAdapter} |
| * will provide the key associated with the given element when |
| * {@link ISharedDocumentAdapter#getDocumentKey(Object)} is called |
| * for any {@link IDocumentRange} node whose document matches the |
| * provided document. Thus, this adapter should also be returned |
| * by the structure comparator and its children when they are adapted |
| * to an {@link ISharedDocumentAdapter}. |
| * @param element the element |
| * @param document the document that has the contents for the element |
| * @param sharedDocumentAdapter the shared document adapter from which the |
| * document was obtained or <code>null</code> if the document |
| * is not shared. |
| * @param monitor a progress monitor or <code>null</code> if progress is not required |
| * |
| * @return a structure comparator |
| * @throws CoreException |
| */ |
| protected abstract IStructureComparator createStructureComparator( |
| final Object element, IDocument document, |
| final ISharedDocumentAdapter sharedDocumentAdapter, |
| IProgressMonitor monitor) throws CoreException; |
| |
| /** |
| * Sets up the newly created document as appropriate. Any document partitioners |
| * should be added to a custom slot using the {@link IDocumentExtension3} interface |
| * in case the document is shared via a file buffer. |
| * @param document a document |
| */ |
| protected void setupDocument(IDocument document) { |
| String partitioning = getDocumentPartitioning(); |
| if (partitioning == null || !(document instanceof IDocumentExtension3)) { |
| if (document.getDocumentPartitioner() == null) { |
| IDocumentPartitioner partitioner= getDocumentPartitioner(); |
| if (partitioner != null) { |
| document.setDocumentPartitioner(partitioner); |
| partitioner.connect(document); |
| } |
| } |
| } else { |
| IDocumentExtension3 ex3 = (IDocumentExtension3) document; |
| if (ex3.getDocumentPartitioner(partitioning) == null) { |
| IDocumentPartitioner partitioner= getDocumentPartitioner(); |
| if (partitioner != null) { |
| ex3.setDocumentPartitioner(partitioning, partitioner); |
| partitioner.connect(document); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the partitioner to be associated with the document or |
| * <code>null</code> is partitioning is not needed or if the subclass |
| * overrode {@link #setupDocument(IDocument)} directly. |
| * @return a partitioner |
| */ |
| protected IDocumentPartitioner getDocumentPartitioner() { |
| return null; |
| } |
| |
| /** |
| * Returns the partitioning to which the partitioner returned from |
| * {@link #getDocumentPartitioner()} is to be associated. Return <code>null</code> |
| * only if partitioning is not needed or if the subclass |
| * overrode {@link #setupDocument(IDocument)} directly. |
| * @see IDocumentExtension3 |
| * @return a partitioning |
| */ |
| protected String getDocumentPartitioning() { |
| return null; |
| } |
| |
| /** |
| * Default implementation of save that extracts the contents from |
| * the document of an {@link IDocumentRange} and sets it on the |
| * input. If the input is an {@link IEncodedStreamContentAccessor}, |
| * the charset of the input is used to extract the contents from the |
| * document. If the input adapts to {@link ISharedDocumentAdapter} and |
| * the document of the {@link IDocumentRange} matches that of the |
| * input, then the save is issued through the shared document adapter. |
| * @see org.eclipse.compare.structuremergeviewer.IStructureCreator#save(org.eclipse.compare.structuremergeviewer.IStructureComparator, java.lang.Object) |
| */ |
| @Override |
| public void save(IStructureComparator node, Object input) { |
| if (node instanceof IDocumentRange && input instanceof IEditableContent) { |
| IDocument document= ((IDocumentRange)node).getDocument(); |
| // First check to see if we have a shared document |
| final ISharedDocumentAdapter sda = SharedDocumentAdapterWrapper.getAdapter(input); |
| if (sda != null) { |
| IEditorInput key = sda.getDocumentKey(input); |
| if (key != null) { |
| IDocumentProvider provider = SharedDocumentAdapter.getDocumentProvider(key); |
| if (provider != null) { |
| IDocument providerDoc = provider.getDocument(key); |
| // We have to make sure that the document we are saving is the same as the shared document |
| if (providerDoc != null && providerDoc == document) { |
| if (save(provider, document, input, sda, key)) |
| return; |
| } |
| } |
| } |
| } |
| IEditableContent bca= (IEditableContent) input; |
| String contents= document.get(); |
| String encoding= null; |
| if (input instanceof IEncodedStreamContentAccessor) { |
| try { |
| encoding= ((IEncodedStreamContentAccessor)input).getCharset(); |
| } catch (CoreException e1) { |
| // ignore |
| } |
| } |
| if (encoding == null) |
| encoding= ResourcesPlugin.getEncoding(); |
| byte[] bytes; |
| try { |
| bytes= contents.getBytes(encoding); |
| } catch (UnsupportedEncodingException e) { |
| bytes= contents.getBytes(); |
| } |
| bca.setContent(bytes); |
| } |
| } |
| |
| private boolean save(final IDocumentProvider provider, final IDocument document, |
| final Object input, final ISharedDocumentAdapter sda, final IEditorInput key) { |
| try { |
| sda.flushDocument(provider, key, document, false); |
| return true; |
| } catch (CoreException e) { |
| CompareUIPlugin.log(e); |
| } |
| return false; |
| } |
| |
| /** |
| * Create an {@link ISharedDocumentAdapter} that will provide the document key for the given input |
| * object for any {@link DocumentRangeNode} instances whose document is the same as the |
| * provided document. |
| * @param input the input element |
| * @param document the document associated with the input element |
| * @return a shared document adapter that provides the proper document key for document range nodes |
| */ |
| private final ISharedDocumentAdapter wrapSharedDocumentAdapter(ISharedDocumentAdapter elementAdapter, final Object input, final IDocument document) { |
| // We need to wrap the adapter so that the proper document key gets returned |
| return new SharedDocumentAdapterWrapper(elementAdapter) { |
| @Override |
| public IEditorInput getDocumentKey(Object element) { |
| if (hasSameDocument(element)) { |
| return super.getDocumentKey(input); |
| } |
| return super.getDocumentKey(element); |
| } |
| private boolean hasSameDocument(Object element) { |
| if (element instanceof DocumentRangeNode) { |
| DocumentRangeNode drn = (DocumentRangeNode) element; |
| return drn.getDocument() == document; |
| } |
| return false; |
| } |
| }; |
| } |
| |
| /** |
| * Default implementation of {@link #createElement(Object, Object, IProgressMonitor)} |
| * that uses {@link #getPath(Object, Object)} to determine the |
| * path for the element, {@link #createStructure(Object, IProgressMonitor)} to create the structure |
| * and {@link #findElement(IStructureComparator, String[])} to find the |
| * element in the structure. Subclasses may override. |
| * @param element the element |
| * @param input the containing input |
| * @param monitor a progress monitor |
| * @return the sub-structure element in the input for the given element |
| * @throws CoreException if a parse error occurred |
| */ |
| @Override |
| public ITypedElement createElement(Object element, Object input, IProgressMonitor monitor) |
| throws CoreException { |
| String[] path= getPath(element, input); |
| if (path == null) { |
| // TODO: Temporary code until subclasses are updated |
| IStructureComparator locate = locate(element, input); |
| if (locate instanceof ITypedElement) { |
| return (ITypedElement)locate; |
| } |
| return null; |
| } |
| |
| // Build the structure |
| IStructureComparator structure= createStructure(input, monitor); |
| if (structure == null) // we couldn't parse the structure |
| return null; // so we can't find anything |
| |
| // find the path in the tree |
| return findElement(structure, path); |
| } |
| |
| /** |
| * Default implementation of {@link #locate(Object, Object)} that |
| * uses {@link #getPath(Object, Object)} to determine the |
| * path for the element, {@link #getStructure(Object)} to create the structure |
| * and {@link #findElement(IStructureComparator, String[])} to find the |
| * element in the structure. Subclasses may override. |
| * @param element the element |
| * @param input the containing input |
| * @return the sub-structure element in the input for the given element |
| */ |
| @Override |
| public IStructureComparator locate(Object element, Object input) { |
| String[] path= getPath(element, input); |
| if (path == null) |
| return null; |
| // Build the structure |
| IStructureComparator structure= getStructure(input); |
| if (structure == null) // we couldn't parse the structure |
| return null; // so we can't find anything |
| |
| // find the path in the tree |
| return (IStructureComparator)findElement(structure, path); |
| } |
| |
| /** |
| * Finds the element at the given path in the given structure. |
| * This method is invoked from the {@link #createElement(Object, Object, IProgressMonitor)} |
| * and {@link #locate(Object, Object)} methods to find the element for |
| * the given path. |
| * @param structure the structure |
| * @param path the path of an element in the structure |
| * @return the element at the given path in the structure or <code>null</code> |
| */ |
| protected ITypedElement findElement(IStructureComparator structure, String[] path) { |
| return (ITypedElement)find(structure, path, 0); |
| } |
| |
| /** |
| * Recursively extracts the given path from the tree. |
| */ |
| private IStructureComparator find(IStructureComparator tree, String[] path, int index) { |
| if (tree != null) { |
| Object[] children= tree.getChildren(); |
| if (children != null) { |
| for (int i= 0; i < children.length; i++) { |
| IStructureComparator child= (IStructureComparator) children[i]; |
| if (child instanceof ITypedElement && child instanceof DocumentRangeNode) { |
| String n1= ((DocumentRangeNode)child).getId(); |
| if (n1 == null) { |
| n1= ((ITypedElement)child).getName(); |
| } |
| String n2= path[index]; |
| if (n1.equals(n2)) { |
| if (index == path.length-1) |
| return child; |
| IStructureComparator result= find(child, path, index+1); |
| if (result != null) |
| return result; |
| } |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the path of the element in the structure of it's containing input |
| * or <code>null</code> if the element is not contained in the input. This method is |
| * invoked from {@link #createElement(Object, Object, IProgressMonitor)} and |
| * {@link #locate(Object, Object)} methods to determine |
| * the path to be passed to {@link #findElement(IStructureComparator, String[])}. |
| * By default, <code>null</code> is returned. Subclasses may override. |
| * @param element the element |
| * @param input the input |
| * @return the path of the element in the structure of it's containing input |
| * or <code>null</code> |
| */ |
| protected String[] getPath(Object element, Object input) { |
| return null; |
| } |
| |
| @Override |
| public void destroy(Object object) { |
| IDisposable disposable = getDisposable(object); |
| if (disposable != null) |
| disposable.dispose(); |
| } |
| |
| private IDisposable getDisposable(Object object) { |
| if (object instanceof IDisposable) { |
| return (IDisposable) object; |
| } |
| if (object instanceof DocumentRangeNode) { |
| DocumentRangeNode node = (DocumentRangeNode) object; |
| return getDisposable(node.getParentNode()); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns true if the two nodes are equal for comparison purposes. If |
| * <code>compareFilters</code> is not empty, the filters are applied to each |
| * line of each node's text representation. |
| * |
| * @param node1 |
| * @param contributor1 |
| * either 'A', 'L', or 'R' for ancestor, left or right |
| * contributor |
| * @param node2 |
| * @param contributor2 |
| * either 'A', 'L', or 'R' for ancestor, left or right |
| * contributor |
| * @param ignoreWhitespace |
| * if <code>true</code> whitespace characters will be ignored |
| * when determining equality. Note: Will bypass any custom ignore |
| * whitespace behaviors contributed through implementations of |
| * <code>org.eclipse.compare.structuremergeviewer.IStructureCreator.getContents()</code> |
| * @param compareFilters |
| * the filters used to customize the comparison of lines of text. |
| * @return whether the two nodes are equal for comparison purposes |
| * @noreference This method is not intended to be referenced by clients. |
| * @since 3.6 |
| */ |
| public boolean contentsEquals(Object node1, char contributor1, |
| Object node2, char contributor2, boolean ignoreWhitespace, |
| ICompareFilter[] compareFilters) { |
| |
| List<String> lines1 = LineReader.readLines(new BufferedReader(new StringReader( |
| getContents(node1, false)))); |
| List<String> lines2 = LineReader.readLines(new BufferedReader(new StringReader( |
| getContents(node2, false)))); |
| |
| StringBuilder buffer1 = new StringBuilder(); |
| StringBuilder buffer2 = new StringBuilder(); |
| |
| int maxLines = Math.max(lines1.size(), lines2.size()); |
| for (int i = 0; i < maxLines; i++) { |
| String s1 = lines1.size() > i ? (String) lines1.get(i) : ""; //$NON-NLS-1$ |
| String s2 = lines2.size() > i ? (String) lines2.get(i) : ""; //$NON-NLS-1$ |
| |
| if (compareFilters != null && compareFilters.length > 0) { |
| s1 = Utilities.applyCompareFilters(s1, contributor1, s2, |
| contributor2, compareFilters); |
| s2 = Utilities.applyCompareFilters(s2, contributor2, s1, |
| contributor1, compareFilters); |
| } |
| buffer1.append(s1); |
| buffer2.append(s2); |
| } |
| if (ignoreWhitespace) { |
| int l1 = buffer1.length(); |
| int l2 = buffer2.length(); |
| int c1 = 0, c2 = 0; |
| int i1 = 0, i2 = 0; |
| |
| while (c1 != -1) { |
| |
| c1 = -1; |
| while (i1 < l1) { |
| char c = buffer1.charAt(i1++); |
| if (!Character.isWhitespace(c)) { |
| c1 = c; |
| break; |
| } |
| } |
| |
| c2 = -1; |
| while (i2 < l2) { |
| char c = buffer2.charAt(i2++); |
| if (!Character.isWhitespace(c)) { |
| c2 = c; |
| break; |
| } |
| } |
| |
| if (c1 != c2) |
| return false; |
| } |
| } else if (!buffer1.toString().equals(buffer2.toString())) { |
| return false; |
| } |
| |
| return true; |
| } |
| } |