blob: 85add5438f0ec338eb40768b9fdfdf60431ad169 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2011 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.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import org.eclipse.compare.IEditableContent;
import org.eclipse.compare.IEditableContentExtension;
import org.eclipse.compare.IEncodedStreamContentAccessor;
import org.eclipse.compare.ISharedDocumentAdapter;
import org.eclipse.compare.IStreamContentAccessor;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.contentmergeviewer.IDocumentRange;
import org.eclipse.compare.internal.CompareUIPlugin;
import org.eclipse.compare.internal.Utilities;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;
import org.eclipse.swt.widgets.Shell;
/**
* A document range node represents a structural element
* when performing a structure compare of documents.
* <code>DocumentRangeNodes</code> are created while parsing the document and represent
* a semantic entity (e.g. a Java class or method).
* As a consequence of the parsing a <code>DocumentRangeNode</code> maps to a range
* of characters in the document.
* <p>
* Since a <code>DocumentRangeNode</code> implements the <code>IStructureComparator</code>
* and <code>IStreamContentAccessor</code> interfaces it can be used as input to the
* differencing engine. This makes it possible to perform
* a structural diff on a document and have the nodes and leaves of the compare easily map
* to character ranges within the document.
* <p>
* Clients need to be aware that this node registers position updaters with the document
* using {@link IDocument#addPosition(String, Position)} with the category set to
* {@link IDocumentRange#RANGE_CATEGORY}. The {@link StructureDiffViewer} will
* remove the category when the nodes are no longer being used. Other clients
* must do the same.
* <p>
* Subclasses may add additional state collected while parsing the document.
* </p>
* @see Differencer
*/
public class DocumentRangeNode
implements IDocumentRange, IStructureComparator, IEditableContent,
IEncodedStreamContentAccessor, IAdaptable, IEditableContentExtension {
private static final String UTF_16= "UTF-16"; //$NON-NLS-1$
private IDocument fBaseDocument;
private Position fRange; // the range in the base document
private int fTypeCode;
private String fID;
private Position fAppendPosition; // a position where to insert a child textually
private ArrayList<DocumentRangeNode> fChildren;
private final DocumentRangeNode fParent;
/**
* Creates a new <code>DocumentRangeNode</code> for the given range within the specified
* document. The <code>typeCode</code> is uninterpreted client data. The ID is used when comparing
* two nodes with each other: i.e. the differencing engine performs a content compare
* on two nodes if their IDs are equal.
*
* @param typeCode a type code for this node
* @param id an identifier for this node
* @param document document on which this node is based on
* @param start start position of range within document
* @param length length of range
*/
public DocumentRangeNode(int typeCode, String id, IDocument document, int start, int length) {
this(null, typeCode, id, document, start, length);
}
/**
* Creates a new <code>DocumentRangeNode</code> for the given range within the specified
* document. The <code>typeCode</code> is uninterpreted client data. The ID is used when comparing
* two nodes with each other: i.e. the differencing engine performs a content compare
* on two nodes if their IDs are equal.
*
* @param parent the parent node
* @param typeCode a type code for this node
* @param id an identifier for this node
* @param document document on which this node is based on
* @param start start position of range within document
* @param length length of range
* @since 3.3
*/
public DocumentRangeNode(DocumentRangeNode parent, int typeCode, String id, IDocument document, int start, int length) {
fParent = parent;
fTypeCode= typeCode;
fID= id;
fBaseDocument= document;
registerPositionUpdater(start, length);
}
private void registerPositionUpdater(int start, int length) {
fBaseDocument.addPositionCategory(RANGE_CATEGORY);
fRange= new Position(start, length);
try {
fBaseDocument.addPosition(RANGE_CATEGORY, fRange);
} catch (BadPositionCategoryException ex) {
CompareUIPlugin.log(ex);
} catch (BadLocationException ex) {
CompareUIPlugin.log(ex);
}
}
/* (non Javadoc)
* see IDocumentRange.getDocument
*/
@Override
public IDocument getDocument() {
return fBaseDocument;
}
/* (non Javadoc)
* see IDocumentRange.getRange
*/
@Override
public Position getRange() {
return fRange;
}
/**
* Returns the type code of this node.
* The type code is uninterpreted client data which can be set in the constructor.
*
* @return the type code of this node
*/
public int getTypeCode() {
return fTypeCode;
}
/**
* Returns this node's id.
* It is used in <code>equals</code> and <code>hashcode</code>.
*
* @return the node's id
*/
public String getId() {
return fID;
}
/**
* Sets this node's id.
* It is used in <code>equals</code> and <code>hashcode</code>.
*
* @param id the new id for this node
*/
public void setId(String id) {
fID= id;
}
/**
* Adds the given node as a child.
*
* @param node the node to add as a child
*/
public void addChild(DocumentRangeNode node) {
if (fChildren == null)
fChildren= new ArrayList<>();
fChildren.add(node);
}
@Override
public Object[] getChildren() {
if (fChildren != null)
return fChildren.toArray();
return new Object[0];
}
/**
* Sets the length of the range of this node.
*
* @param length the length of the range
*/
public void setLength(int length) {
getRange().setLength(length);
}
/**
* Sets a position within the document range that can be used to (legally) insert
* text without breaking the syntax of the document.
* <p>
* E.g. when parsing a Java document the "append position" of a <code>DocumentRangeNode</code>
* representing a Java class could be the character position just before the closing bracket.
* Inserting the text of a new method there would not disturb the syntax of the class.
*
* @param pos the character position within the underlying document where text can be legally inserted
*/
public void setAppendPosition(int pos) {
if (fAppendPosition != null)
try {
fBaseDocument.removePosition(RANGE_CATEGORY, fAppendPosition);
} catch (BadPositionCategoryException e) {
// Ignore
}
try {
// TODO: Avoid an exception for a position that is past the end of the document
if (pos <= getDocument().getLength()) {
Position p= new Position(pos);
fBaseDocument.addPosition(RANGE_CATEGORY, p);
fAppendPosition= p;
}
} catch (BadPositionCategoryException ex) {
// silently ignored
} catch (BadLocationException ex) {
// silently ignored
}
}
/**
* Returns the position that has been set with <code>setAppendPosition</code>.
* If <code>setAppendPosition</code> hasn't been called, the position after the last character
* of this range is returned. This method will return <code>null</code> if the position
* could not be registered with the document.
*
* @return a position where text can be legally inserted
*/
public Position getAppendPosition() {
if (fAppendPosition == null) {
try {
Position p= new Position(fBaseDocument.getLength());
fBaseDocument.addPosition(RANGE_CATEGORY, p);
fAppendPosition= p;
return fAppendPosition;
} catch (BadPositionCategoryException ex) {
// silently ignored
} catch (BadLocationException ex) {
// silently ignored
}
}
return new Position(fBaseDocument.getLength());
}
/**
* Implementation based on <code>getID</code>.
* @param other the object to compare this <code>DocumentRangeNode</code> against.
* @return <code>true</code> if the <code>DocumentRangeNodes</code>are equal; <code>false</code> otherwise.
*/
@Override
public boolean equals(Object other) {
if (other != null && other.getClass() == getClass()) {
DocumentRangeNode tn= (DocumentRangeNode) other;
return fTypeCode == tn.fTypeCode && fID.equals(tn.fID);
}
return super.equals(other);
}
/**
* Implementation based on <code>getID</code>.
* @return a hash code for this object.
*/
@Override
public int hashCode() {
return fID.hashCode();
}
/*
* Find corresponding position
*/
private Position findCorrespondingPosition(DocumentRangeNode otherParent, DocumentRangeNode child) {
// we try to find a predecessor of left Node which exists on the right side
if (child != null && fChildren != null) {
int ix= otherParent.fChildren.indexOf(child);
if (ix >= 0) {
for (int i= ix - 1; i >= 0; i--) {
DocumentRangeNode c1= otherParent.fChildren.get(i);
int i2= fChildren.indexOf(c1);
if (i2 >= 0) {
DocumentRangeNode c= fChildren.get(i2);
//System.out.println(" found corresponding: " + i2 + " " + c);
Position p= c.fRange;
//try {
Position po= new Position(p.getOffset() + p.getLength() + 1, 0);
//c.fBaseDocument.addPosition(RANGE_CATEGORY, po);
return po;
//} catch (BadLocationException ex) {
//}
//break;
}
}
for (int i= ix; i < otherParent.fChildren.size(); i++) {
DocumentRangeNode c1= otherParent.fChildren.get(i);
int i2= fChildren.indexOf(c1);
if (i2 >= 0) {
DocumentRangeNode c= fChildren.get(i2);
//System.out.println(" found corresponding: " + i2 + " " + c);
Position p= c.fRange;
//try {
Position po= new Position(p.getOffset(), 0);
//c.fBaseDocument.addPosition(RANGE_CATEGORY, po);
return po;
//} catch (BadLocationException ex) {
//}
//break;
}
}
}
}
return getAppendPosition();
}
private void add(String s, DocumentRangeNode parent, DocumentRangeNode child) {
Position p= findCorrespondingPosition(parent, child);
if (p != null) {
try {
fBaseDocument.replace(p.getOffset(), p.getLength(), s);
} catch (BadLocationException ex) {
CompareUIPlugin.log(ex);
}
}
}
/* (non Javadoc)
* see IStreamContentAccessor.getContents
*/
@Override
public InputStream getContents() {
String s;
try {
s= fBaseDocument.get(fRange.getOffset(), fRange.getLength());
} catch (BadLocationException ex) {
s= ""; //$NON-NLS-1$
}
return new ByteArrayInputStream(Utilities.getBytes(s, UTF_16));
}
/**
* If this node has a parent, return the editability of the parent.
* Otherwise return <code>true</code>. Subclasses may override.
* @see org.eclipse.compare.IEditableContent#isEditable()
*/
@Override
public boolean isEditable() {
if (fParent != null)
return fParent.isEditable();
return true;
}
@Override
public ITypedElement replace(ITypedElement child, ITypedElement other) {
if (fParent == null) {
// TODO: I don't believe this code does anything useful but just in case
// I'm leaving it in but disabling it for the shared document case
// since all the subclasses that have been converted overrode the method anyway
DocumentRangeNode src= null;
String srcContents= ""; //$NON-NLS-1$
if (other != null) {
src= (DocumentRangeNode) child;
if (other instanceof IStreamContentAccessor) {
try {
srcContents= Utilities.readString((IStreamContentAccessor)other);
} catch(CoreException ex) {
// NeedWork
CompareUIPlugin.log(ex);
}
}
}
if (child == null) // no destination: we have to add the contents into the parent
add(srcContents, null, src);
}
nodeChanged(this);
return child;
}
/**
* Default implementation that calls {@link #internalSetContents(byte[])}
* and then {@link #nodeChanged(DocumentRangeNode)}. Subclasses
* may override but should then call {@link #nodeChanged(DocumentRangeNode)}
* after the contents have been set.
* @see org.eclipse.compare.IEditableContent#setContent(byte[])
*/
@Override
public void setContent(byte[] content) {
internalSetContents(content);
nodeChanged(this);
}
/**
* Method that is invoked from {@link #setContent(byte[])}. By default,
* this method does nothing. Subclasses may override.
* @param content the new content
* @since 3.3
*/
protected void internalSetContents(byte[] content) {
// By default, do nothing
}
@Override
public String getCharset() {
return UTF_16;
}
/**
* Method that should be invoked whenever the contents of this node are
* changed. the change is propagated to the parent if there is one.
* @param node the node that has changed.
* @since 3.3
*/
protected void nodeChanged(DocumentRangeNode node) {
if (fParent != null)
fParent.nodeChanged(node);
}
/**
* Implement {@link IAdaptable#getAdapter(Class)} in order to provide
* an {@link ISharedDocumentAdapter} that provides the proper look up key based
* on the input from which this structure node was created. The proper
* shared document adapter is obtained by calling {@link #getAdapter(Class)}
* on this node's parent if there is one.
* @param adapter the adapter class to look up
* @return the object adapted to the given class or <code>null</code>
* @see IAdaptable#getAdapter(Class)
* @since 3.3
*/
@Override
public <T> T getAdapter(Class<T> adapter) {
if (adapter == ISharedDocumentAdapter.class && fParent != null)
return fParent.getAdapter(adapter);
return Platform.getAdapterManager().getAdapter(this, adapter);
}
@Override
public boolean isReadOnly() {
if (fParent != null)
return fParent.isReadOnly();
return false;
}
@Override
public IStatus validateEdit(Shell shell) {
if (fParent != null)
return fParent.validateEdit(shell);
return Status.OK_STATUS;
}
/**
* Return the parent of this node or <code>null</code>
* if the node doesn't have a parent or the parent is not known.
* @return the parent of this node or <code>null</code>
*/
public Object getParentNode() {
return fParent;
}
}