/******************************************************************************* * Copyright (c) 2000, 2008 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.jdt.internal.core; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.eclipse.jdt.core.IAnnotation; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaElementDelta; import org.eclipse.jdt.core.IParent; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.internal.core.util.Util; /** * A java element delta builder creates a java element delta on a java element between the version * of the java element at the time the comparator was created and the current version of the java * element. * * It performs this operation by locally caching the contents of the java element when it is * created. When the method createDeltas() is called, it creates a delta over the cached contents * and the new contents. */ public class JavaElementDeltaBuilder { /** * The java element handle */ IJavaElement javaElement; /** * The maximum depth in the java element children we should look into */ int maxDepth= Integer.MAX_VALUE; /** * The old handle to info relationships */ Map infos; Map annotationInfos; /** * The old position info */ Map oldPositions; /** * The new position info */ Map newPositions; /** * Change delta */ public JavaElementDelta delta= null; /** * List of added elements */ HashSet added; /** * List of removed elements */ HashSet removed; /** * Doubly linked list item */ static class ListItem { public IJavaElement previous; public IJavaElement next; public ListItem(IJavaElement previous, IJavaElement next) { this.previous= previous; this.next= next; } } /** * Creates a java element comparator on a java element looking as deep as necessary. */ public JavaElementDeltaBuilder(IJavaElement javaElement) { this.javaElement= javaElement; initialize(); recordElementInfo( javaElement, (JavaModel)this.javaElement.getJavaModel(), 0); } /** * Creates a java element comparator on a java element looking only 'maxDepth' levels deep. */ public JavaElementDeltaBuilder(IJavaElement javaElement, int maxDepth) { this.javaElement= javaElement; this.maxDepth= maxDepth; initialize(); recordElementInfo( javaElement, (JavaModel)this.javaElement.getJavaModel(), 0); } /** * Repairs the positioning information after an element has been added */ private void added(IJavaElement element) { this.added.add(element); ListItem current= getNewPosition(element); ListItem previous= null, next= null; if (current.previous != null) previous= getNewPosition(current.previous); if (current.next != null) next= getNewPosition(current.next); if (previous != null) previous.next= current.next; if (next != null) next.previous= current.previous; } /** * Builds the java element deltas between the old content of the compilation unit and its new * content. */ public void buildDeltas() { this.delta= new JavaElementDelta(this.javaElement); // if building a delta on a compilation unit or below, // it's a fine grained delta if (this.javaElement.getElementType() >= IJavaElement.COMPILATION_UNIT) { this.delta.fineGrained(); } recordNewPositions(this.javaElement, 0); findAdditions(this.javaElement, 0); findDeletions(); findChangesInPositioning(this.javaElement, 0); trimDelta(this.delta); if (this.delta.getAffectedChildren().length == 0) { // this is a fine grained but not children affected -> mark as content changed this.delta.contentChanged(); } } private boolean equals(char[][][] first, char[][][] second) { if (first == second) return true; if (first == null || second == null) return false; if (first.length != second.length) return false; for (int i= first.length; --i >= 0;) if (!CharOperation.equals(first[i], second[i])) return false; return true; } /** * Finds elements which have been added or changed. */ private void findAdditions(IJavaElement newElement, int depth) { JavaElementInfo oldInfo= getElementInfo(newElement); if (oldInfo == null && depth < this.maxDepth) { this.delta.added(newElement); added(newElement); } else { removeElementInfo(newElement); } if (depth >= this.maxDepth) { // mark element as changed this.delta.changed(newElement, IJavaElementDelta.F_CONTENT); return; } JavaElementInfo newInfo= null; try { newInfo= (JavaElementInfo)((JavaElement)newElement).getElementInfo(); } catch (JavaModelException npe) { return; } findContentChange(oldInfo, newInfo, newElement); if (oldInfo != null && newElement instanceof IParent) { IJavaElement[] children= newInfo.getChildren(); if (children != null) { int length= children.length; for (int i= 0; i < length; i++) { findAdditions(children[i], depth + 1); } } } } /** * Looks for changed positioning of elements. */ private void findChangesInPositioning(IJavaElement element, int depth) { if (depth >= this.maxDepth || this.added.contains(element) || this.removed.contains(element)) return; if (!isPositionedCorrectly(element)) { this.delta.changed(element, IJavaElementDelta.F_REORDER); } if (element instanceof IParent) { JavaElementInfo info= null; try { info= (JavaElementInfo)((JavaElement)element).getElementInfo(); } catch (JavaModelException npe) { return; } IJavaElement[] children= info.getChildren(); if (children != null) { int length= children.length; for (int i= 0; i < length; i++) { findChangesInPositioning(children[i], depth + 1); } } } } private void findAnnotationChanges(IAnnotation[] oldAnnotations, IAnnotation[] newAnnotations, IJavaElement parent) { ArrayList annotationDeltas= null; for (int i= 0, length= newAnnotations.length; i < length; i++) { IAnnotation newAnnotation= newAnnotations[i]; Object oldInfo= this.annotationInfos.remove(newAnnotation); if (oldInfo == null) { JavaElementDelta annotationDelta= new JavaElementDelta(newAnnotation); annotationDelta.added(); if (annotationDeltas == null) annotationDeltas= new ArrayList(); annotationDeltas.add(annotationDelta); continue; } else { AnnotationInfo newInfo= null; try { newInfo= (AnnotationInfo)((JavaElement)newAnnotation).getElementInfo(); } catch (JavaModelException npe) { return; } if (!Util.equalArraysOrNull(((AnnotationInfo)oldInfo).members, newInfo.members)) { JavaElementDelta annotationDelta= new JavaElementDelta(newAnnotation); annotationDelta.changed(IJavaElementDelta.F_CONTENT); if (annotationDeltas == null) annotationDeltas= new ArrayList(); annotationDeltas.add(annotationDelta); } } } for (int i= 0, length= oldAnnotations.length; i < length; i++) { IAnnotation oldAnnotation= oldAnnotations[i]; if (this.annotationInfos.remove(oldAnnotation) != null) { JavaElementDelta annotationDelta= new JavaElementDelta(oldAnnotation); annotationDelta.removed(); if (annotationDeltas == null) annotationDeltas= new ArrayList(); annotationDeltas.add(annotationDelta); } } if (annotationDeltas == null) return; int size= annotationDeltas.size(); if (size > 0) { JavaElementDelta parentDelta= this.delta.changed(parent, IJavaElementDelta.F_ANNOTATIONS); parentDelta.annotationDeltas= (IJavaElementDelta[])annotationDeltas.toArray(new IJavaElementDelta[size]); } } /** * The elements are equivalent, but might have content changes. */ private void findContentChange(JavaElementInfo oldInfo, JavaElementInfo newInfo, IJavaElement newElement) { if (oldInfo instanceof MemberElementInfo && newInfo instanceof MemberElementInfo) { if (((MemberElementInfo)oldInfo).getModifiers() != ((MemberElementInfo)newInfo).getModifiers()) { this.delta.changed(newElement, IJavaElementDelta.F_MODIFIERS); } if (oldInfo instanceof AnnotatableInfo && newInfo instanceof AnnotatableInfo) { findAnnotationChanges(((AnnotatableInfo)oldInfo).annotations, ((AnnotatableInfo)newInfo).annotations, newElement); } if (oldInfo instanceof SourceMethodElementInfo && newInfo instanceof SourceMethodElementInfo) { SourceMethodElementInfo oldSourceMethodInfo= (SourceMethodElementInfo)oldInfo; SourceMethodElementInfo newSourceMethodInfo= (SourceMethodElementInfo)newInfo; if (!CharOperation.equals(oldSourceMethodInfo.getReturnTypeName(), newSourceMethodInfo.getReturnTypeName()) || !CharOperation.equals(oldSourceMethodInfo.getTypeParameterNames(), newSourceMethodInfo.getTypeParameterNames()) || !equals(oldSourceMethodInfo.getTypeParameterBounds(), newSourceMethodInfo.getTypeParameterBounds())) { this.delta.changed(newElement, IJavaElementDelta.F_CONTENT); } } else if (oldInfo instanceof SourceFieldElementInfo && newInfo instanceof SourceFieldElementInfo) { if (!CharOperation.equals( ((SourceFieldElementInfo)oldInfo).getTypeName(), ((SourceFieldElementInfo)newInfo).getTypeName())) { this.delta.changed(newElement, IJavaElementDelta.F_CONTENT); } } else if (oldInfo instanceof SourceTypeElementInfo && newInfo instanceof SourceTypeElementInfo) { SourceTypeElementInfo oldSourceTypeInfo= (SourceTypeElementInfo)oldInfo; SourceTypeElementInfo newSourceTypeInfo= (SourceTypeElementInfo)newInfo; if (!CharOperation.equals(oldSourceTypeInfo.getSuperclassName(), newSourceTypeInfo.getSuperclassName()) || !CharOperation.equals(oldSourceTypeInfo.getInterfaceNames(), newSourceTypeInfo.getInterfaceNames())) { this.delta.changed(newElement, IJavaElementDelta.F_SUPER_TYPES); } if (!CharOperation.equals(oldSourceTypeInfo.getTypeParameterNames(), newSourceTypeInfo.getTypeParameterNames()) || !equals(oldSourceTypeInfo.getTypeParameterBounds(), newSourceTypeInfo.getTypeParameterBounds())) { this.delta.changed(newElement, IJavaElementDelta.F_CONTENT); } HashMap oldTypeCategories= oldSourceTypeInfo.categories; HashMap newTypeCategories= newSourceTypeInfo.categories; if (oldTypeCategories != null) { // take the union of old and new categories elements (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=125675) Set elements; if (newTypeCategories != null) { elements= new HashSet(oldTypeCategories.keySet()); elements.addAll(newTypeCategories.keySet()); } else elements= oldTypeCategories.keySet(); Iterator iterator= elements.iterator(); while (iterator.hasNext()) { IJavaElement element= (IJavaElement)iterator.next(); String[] oldCategories= (String[])oldTypeCategories.get(element); String[] newCategories= newTypeCategories == null ? null : (String[])newTypeCategories.get(element); if (!Util.equalArraysOrNull(oldCategories, newCategories)) { this.delta.changed(element, IJavaElementDelta.F_CATEGORIES); } } } else if (newTypeCategories != null) { Iterator elements= newTypeCategories.keySet().iterator(); while (elements.hasNext()) { IJavaElement element= (IJavaElement)elements.next(); this.delta.changed(element, IJavaElementDelta.F_CATEGORIES); // all categories for this element were removed } } } } } /** * Adds removed deltas for any handles left in the table */ private void findDeletions() { Iterator iter= this.infos.keySet().iterator(); while (iter.hasNext()) { IJavaElement element= (IJavaElement)iter.next(); this.delta.removed(element); removed(element); } } private JavaElementInfo getElementInfo(IJavaElement element) { return (JavaElementInfo)this.infos.get(element); } private ListItem getNewPosition(IJavaElement element) { return (ListItem)this.newPositions.get(element); } private ListItem getOldPosition(IJavaElement element) { return (ListItem)this.oldPositions.get(element); } private void initialize() { this.infos= new HashMap(20); this.oldPositions= new HashMap(20); this.newPositions= new HashMap(20); this.oldPositions.put(this.javaElement, new ListItem(null, null)); this.newPositions.put(this.javaElement, new ListItem(null, null)); this.added= new HashSet(5); this.removed= new HashSet(5); } /** * Inserts position information for the elements into the new or old positions table */ private void insertPositions(IJavaElement[] elements, boolean isNew) { int length= elements.length; IJavaElement previous= null, current= null, next= (length > 0) ? elements[0] : null; for (int i= 0; i < length; i++) { previous= current; current= next; next= (i + 1 < length) ? elements[i + 1] : null; if (isNew) { this.newPositions.put(current, new ListItem(previous, next)); } else { this.oldPositions.put(current, new ListItem(previous, next)); } } } /** * Returns whether the elements position has not changed. */ private boolean isPositionedCorrectly(IJavaElement element) { ListItem oldListItem= getOldPosition(element); if (oldListItem == null) return false; ListItem newListItem= getNewPosition(element); if (newListItem == null) return false; IJavaElement oldPrevious= oldListItem.previous; IJavaElement newPrevious= newListItem.previous; if (oldPrevious == null) { return newPrevious == null; } else { return oldPrevious.equals(newPrevious); } } /** * Records this elements info, and attempts to record the info for the children. */ private void recordElementInfo(IJavaElement element, JavaModel model, int depth) { if (depth >= this.maxDepth) { return; } JavaElementInfo info= (JavaElementInfo)JavaModelManager.getJavaModelManager().getInfo(element); if (info == null) // no longer in the java model. return; this.infos.put(element, info); if (element instanceof IParent) { IJavaElement[] children= info.getChildren(); if (children != null) { insertPositions(children, false); for (int i= 0, length= children.length; i < length; i++) recordElementInfo(children[i], model, depth + 1); } } IAnnotation[] annotations= null; if (info instanceof AnnotatableInfo) annotations= ((AnnotatableInfo)info).annotations; if (annotations != null) { if (this.annotationInfos == null) this.annotationInfos= new HashMap(); JavaModelManager manager= JavaModelManager.getJavaModelManager(); for (int i= 0, length= annotations.length; i < length; i++) { this.annotationInfos.put(annotations[i], manager.getInfo(annotations[i])); } } } /** * Fills the newPositions hashtable with the new position information */ private void recordNewPositions(IJavaElement newElement, int depth) { if (depth < this.maxDepth && newElement instanceof IParent) { JavaElementInfo info= null; try { info= (JavaElementInfo)((JavaElement)newElement).getElementInfo(); } catch (JavaModelException npe) { return; } IJavaElement[] children= info.getChildren(); if (children != null) { insertPositions(children, true); for (int i= 0, length= children.length; i < length; i++) { recordNewPositions(children[i], depth + 1); } } } } /** * Repairs the positioning information after an element has been removed */ private void removed(IJavaElement element) { this.removed.add(element); ListItem current= getOldPosition(element); ListItem previous= null, next= null; if (current.previous != null) previous= getOldPosition(current.previous); if (current.next != null) next= getOldPosition(current.next); if (previous != null) previous.next= current.next; if (next != null) next.previous= current.previous; } private void removeElementInfo(IJavaElement element) { this.infos.remove(element); } public String toString() { StringBuffer buffer= new StringBuffer(); buffer.append("Built delta:\n"); //$NON-NLS-1$ buffer.append(this.delta == null ? "<null>" : this.delta.toString()); //$NON-NLS-1$ return buffer.toString(); } /** * Trims deletion deltas to only report the highest level of deletion */ private void trimDelta(JavaElementDelta elementDelta) { if (elementDelta.getKind() == IJavaElementDelta.REMOVED) { IJavaElementDelta[] children= elementDelta.getAffectedChildren(); for (int i= 0, length= children.length; i < length; i++) { elementDelta.removeAffectedChild((JavaElementDelta)children[i]); } } else { IJavaElementDelta[] children= elementDelta.getAffectedChildren(); for (int i= 0, length= children.length; i < length; i++) { trimDelta((JavaElementDelta)children[i]); } } } }