| /******************************************************************************* |
| * 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; |
| } |
| } |
| |