blob: 7e0a209a65b2ecfb82fa6bd48560760e59ec25c9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* 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 if creating the comparator failed; depends on actual implementation
*/
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 (Object c : children) {
IStructureComparator child = (IStructureComparator) c;
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 first node
* @param contributor1 either 'A', 'L', or 'R' for ancestor, left or right
* contributor
* @param node2 second node
* @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;
}
}