/* This file is part of Green. * * Copyright (C) 2005 The Research Foundation of State University of New York * All Rights Under Copyright Reserved, The Research Foundation of S.U.N.Y. * * Green is free software, licensed under the terms of the Eclipse * Public License, version 1.0. The license is available at * http://www.eclipse.org/legal/epl-v10.html */ package edu.buffalo.cse.green; import static org.eclipse.jdt.core.IJavaElement.COMPILATION_UNIT; import static org.eclipse.jdt.core.IJavaElement.JAVA_PROJECT; import static org.eclipse.jdt.core.IJavaElement.PACKAGE_FRAGMENT; import static org.eclipse.jdt.core.IJavaElement.TYPE; import static org.eclipse.jdt.core.IJavaElementDelta.ADDED; import static org.eclipse.jdt.core.IJavaElementDelta.REMOVED; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.jdt.core.ElementChangedEvent; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IElementChangedListener; import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaElementDelta; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IPackageFragment; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.internal.core.CompilationUnit; import org.eclipse.jdt.internal.core.JavaProject; import org.eclipse.jdt.internal.core.PackageFragment; import org.eclipse.jdt.internal.core.SourceField; import org.eclipse.jdt.internal.core.SourceMethod; import org.eclipse.jdt.internal.core.SourceType; import edu.buffalo.cse.green.editor.DiagramEditor; import edu.buffalo.cse.green.editor.controller.FieldPart; import edu.buffalo.cse.green.editor.controller.MethodPart; import edu.buffalo.cse.green.editor.controller.RootPart; import edu.buffalo.cse.green.editor.model.AbstractModel; import edu.buffalo.cse.green.editor.model.FieldModel; import edu.buffalo.cse.green.editor.model.MethodModel; import edu.buffalo.cse.green.editor.model.RootModel; import edu.buffalo.cse.green.editor.model.TypeModel; /** * Handles changes to the <code>JavaModel</code>, reflecting them in the * affected editors. * * The elementChanged() method is passed in a <code>JavaElementDelta</code> * containing a tree structure with information about the change that occurred * in the <code>JavaModel</code>. * * Helper methods are called that traverse the delta structure and find changes * that correspond to the addition, removal, or movement of certain element * types. Once these changes are detected, each editor is checked to see if the * changes affect it. If they do, the editor's model's references to the changed * elements are updated. Refresh methods are then called on the appropriate edit * parts to update the editor's display. * * @author bcmartin */ public class JavaModelListener implements IElementChangedListener { private static JavaModelListener _listener = new JavaModelListener(); private static Map<Class, RefactorHandler> map; static { // add the elements to consider changes for to a list map = new HashMap<Class, RefactorHandler>(); map.put(JavaProject.class, ProjectRefactorHandler.instance()); map.put(PackageFragment.class, PackageRefactorHandler.instance()); map.put(CompilationUnit.class, CompilationUnitRefactorHandler.instance()); map.put(SourceType.class, TypeRefactorHandler.instance()); map.put(SourceField.class, FieldRefactorHandler.instance()); map.put(SourceMethod.class, MethodRefactorHandler.instance()); } // singleton private JavaModelListener() {} /** * @return The singleton instance of the listener. */ public static JavaModelListener getListener() { return _listener; } /** * Called when an IJavaElement changes. Used to reflect changes in the * editor, which has a view that is based on the <code>JavaModel</code>. * * @param event - The change that occurred to the <code>JavaModel</code>. */ @SuppressWarnings("unchecked") public void elementChanged(ElementChangedEvent event) { try { /* Goes through these classes looking for any that are added, moved * or removed. Calls methods that updates the editor to reflect any * changes found. */ for (Class type : map.keySet()) { List<IJavaElementDelta> added = findAddedElements(event.getDelta(), type); List<IJavaElementDelta> removed = findRemovedElements(event.getDelta(), type); List<IJavaElementDelta> changed = findChangedElements(event.getDelta(), type); HashMap<IJavaElement, IJavaElement> moved = extractMovedElements(added, removed); // ignore updating the editors if no changes occurred if (added.size() == 0 && removed.size() == 0 && moved.size() == 0 && changed.size() == 0) { continue; } List<DiagramEditor> editors = new ArrayList<DiagramEditor>(DiagramEditor.getEditors()); // handle changes for (DiagramEditor editor : editors) { RootModel root = editor.getRootModel(); // handle moves for (IJavaElement sourceElement : moved.keySet()) { IJavaElement targetElement = moved.get(sourceElement); map.get(sourceElement.getClass()).handleMove( root, sourceElement, targetElement); } // handle removes for (IJavaElementDelta removedElement : removed) { map.get(removedElement.getElement().getClass()) .handleRemove(root, removedElement.getElement()); } // handle adds for (IJavaElementDelta addedElement : added) { map.get(addedElement.getElement().getClass()).handleAdd( root, addedElement.getElement()); } // handle changes (to modifiers, etc.) for (IJavaElementDelta changedElement : changed) { handleElementChange(changedElement); } editor.forceRefreshRelationships(); } } } catch (Throwable t) { //TODO Incremental exploration throws Null Pointer. Virtually unreproduceable. GreenException.critical(t); } finally { TypeRefactorHandler.REMOVED_TYPE = null; } } /** * Finds pairs of element changes that triggered "move" events. The types of * elements that are considered eligible for such an event are: * <code>IJavaProject</code> * <code>IPackageFragment</code> * <code>ICompilationUnit</code> * * This is checked using the getMovedFromElement() and getMovedToElement() * methods in the delta. * * There are other <code>IJavaElement</code> subclasses that may trigger * move events; however, in that context, move events do not pertain to the * <code>DiagramEditor</code>. * * @param added - A list of elements that were added. * @param removed - A list of elements that were removed. * @return A mapping of all elements that triggered "move" events in the * <code>IJavaElementDelta</code> hierarchy. */ private HashMap<IJavaElement, IJavaElement> extractMovedElements( List<IJavaElementDelta> added, List<IJavaElementDelta> removed) { HashMap<IJavaElement, IJavaElement> moved = new HashMap<IJavaElement, IJavaElement>(); List<IJavaElementDelta> notAdded = new ArrayList<IJavaElementDelta>(); List<IJavaElementDelta> notRemoved = new ArrayList<IJavaElementDelta>(); for (IJavaElementDelta toDelta : added) { // ignore this element if it wasn't moved to somewhere if (toDelta.getMovedFromElement() != null) { IJavaElement newElement = toDelta.getElement(); // see if there's an element that has // the "newElement" returned by // toDelta.getMovedToElement() for (IJavaElementDelta fromDelta : removed) { // if this element was moved from "newElement"... if (sameElements( fromDelta.getMovedToElement(), newElement)) { IJavaElement oldElement = fromDelta.getElement(); // and this element was moved from "oldElement" if (sameElements( toDelta.getMovedFromElement(), oldElement)) { /* * remove both elements from their respective lists * and put them in the move map */ moved.put(oldElement, newElement); notAdded.add(toDelta); notRemoved.add(fromDelta); } } } } } added.removeAll(notAdded); removed.removeAll(notRemoved); return moved; } /** * Iterates over all subbranches of the <code>IJavaElementDelta</code> and * determines what elements have been added. * * @param parentDelta - The tree (or subtree) of changes. * @param type - The class of elements to check for. * @return A list of added element deltas. */ private List<IJavaElementDelta> findAddedElements( IJavaElementDelta parentDelta, Class type) { List<IJavaElementDelta> changes = new ArrayList<IJavaElementDelta>(); // check for added element if (parentDelta.getKind() == ADDED) { if (type.isInstance(parentDelta.getElement())) { changes.add(parentDelta); } } // traverse all changed branches // this code shouldn't need altering for (IJavaElementDelta delta : parentDelta.getAffectedChildren()) { changes.addAll(findAddedElements(delta, type)); } return changes; } /** * Iterates over all subbranches of the <code>IJavaElementDelta</code> and * determines what elements have changed that are present in the * <code>DiagramEditor</code>. * * @param parentDelta - The delta to look through. * @param type - The class to match. * @return A mapping of moved elements. */ private List<IJavaElementDelta> findChangedElements( IJavaElementDelta parentDelta, Class type) { IJavaElementDelta delta; List<IJavaElementDelta> changes = new ArrayList<IJavaElementDelta>(); // adds deltas representing the removed elements of // the specified type to the list of changes for (int i = 0; i < parentDelta.getChangedChildren().length; i++) { delta = parentDelta.getChangedChildren()[i]; if (type.isInstance(delta.getElement())) { if (delta.getChangedChildren().length == 0) { changes.add(delta); } } } // traverse all changed branches // this code shouldn't need altering for (int i = 0; i < parentDelta.getChangedChildren().length; i++) { delta = parentDelta.getChangedChildren()[i]; changes.addAll(findChangedElements(delta, type)); } return changes; } /** * Iterates over all subbranches of the <code>IJavaElementDelta</code> and * determines what elements have been removed that are present in the * <code>DiagramEditor</code>. * * @param parentDelta - The tree (or subtree) of changes. * @param type - The class of elements to check for. * @return A list of removed element deltas. */ private List<IJavaElementDelta> findRemovedElements( IJavaElementDelta parentDelta, Class type) { List<IJavaElementDelta> changes = new ArrayList<IJavaElementDelta>(); // check for removed element if (parentDelta.getKind() == REMOVED) { if (type.isInstance(parentDelta.getElement())) { changes.add(parentDelta); } } // traverse all changed branches // this code shouldn't need altering for (IJavaElementDelta delta : parentDelta.getAffectedChildren()) { changes.addAll(findRemovedElements(delta, type)); } return changes; } /** * Updates the display when some change occurs to a Java element that * doesn't involve removal, addition, or movement. * * @param elementDelta - The delta of the element that changed. */ private void handleElementChange(IJavaElementDelta elementDelta) { IJavaElement element = elementDelta.getElement(); // update the modifiers of the element (if they changed) if ((elementDelta.getFlags() & IJavaElementDelta.F_MODIFIERS) != 0) { for (DiagramEditor editor : DiagramEditor.getEditors()) { RootPart rootEditPart = editor.getRootPart(); RootModel root = (RootModel) rootEditPart.getModel(); AbstractModel abstractModel = root.getModelFromElement(element); if (abstractModel != null) { if (abstractModel instanceof FieldModel) { FieldModel fModel = (FieldModel) abstractModel; FieldPart fEditPart = (FieldPart) rootEditPart .getPartFromModel(fModel); fEditPart.updateIcon(); } else if (abstractModel instanceof MethodModel) { MethodModel mModel = (MethodModel) abstractModel; MethodPart mEditPart = (MethodPart) rootEditPart .getPartFromModel(mModel); mEditPart.updateIcon(); } } } } } /** * Compares two elements by their handle identifiers. * * @param e1 - An element to compare. * @param e2 - An element to compare. * @return True if the elements have the same handles, false otherwise. */ public static boolean sameElements(IJavaElement e1, IJavaElement e2) { if (e1 == null || e2 == null) return false; return (e1.getHandleIdentifier().equals(e2.getHandleIdentifier())); } } /** * Provides the listener with a polymorphic way to update Green's editor by * mapping different <code>IJavaElement</code> instances onto the classes that * handle their refactoring. * * @author bcmartin * @param <E> - The element handled by the given implementor. */ interface RefactorHandler<E extends IJavaElement> { /** * Handles the addition of an <code>IJavaElement</code> to the workspace. * * @param root * @param element */ public void handleAdd(RootModel root, E element); /** * Handles movement of an <code>IJavaElement</code> within the workspace. * * @param root * @param sourceElement * @param targetElement */ public void handleMove(RootModel root, E sourceElement, E targetElement); /** * Handles the removal of an <code>IJavaElement</code> from the workspace. * * @param root * @param element */ public void handleRemove(RootModel root, E element); } /** * Handles project refactoring. * * @author bcmartin * @param <E> - The <code>IJavaProject</code> implementation to use. */ class ProjectRefactorHandler<E extends IJavaProject> implements RefactorHandler<E> { private static final ProjectRefactorHandler<IJavaProject> INSTANCE = new ProjectRefactorHandler<IJavaProject>(); private ProjectRefactorHandler() {} /** * @see edu.buffalo.cse.green.RefactorHandler#handleAdd(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleAdd(RootModel root, E element) { // do nothing } /** * @return The singleton instance of this <code>RefactorHandler</code>. */ public static RefactorHandler<IJavaProject> instance() { return INSTANCE; } /** * @see edu.buffalo.cse.green.RefactorHandler#handleMove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement, org.eclipse.jdt.core.IJavaElement) */ public void handleMove(RootModel root, E sourceElement, E targetElement) { root.changeProjectElement(sourceElement, targetElement); try { List<IJavaElement> packages = root.getElementsOfKind( PACKAGE_FRAGMENT); // look through packages to match up source and destination elements for (IJavaElement packElement : packages) { IPackageFragment packFrag = (IPackageFragment) packElement; if (JavaModelListener.sameElements(packFrag.getAncestor( JAVA_PROJECT), sourceElement.getPrimaryElement())) { for (IPackageFragment packFrags : targetElement.getPackageFragments()) { // check for the same name of the two packages if (packFrag.getElementName().equals( packFrags.getElementName())) { PackageRefactorHandler.instance().handleMove( root, packFrag, packFrags); } } } } root.dispose(); } catch (JavaModelException e) { e.printStackTrace(); } } /** * @see edu.buffalo.cse.green.RefactorHandler#handleRemove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleRemove(RootModel root, E element) { List<IJavaElement> packages = root.getElementsOfKind(PACKAGE_FRAGMENT); for (IJavaElement pack : packages) { // get the project that contains the package IJavaElement proj = pack.getAncestor(JAVA_PROJECT); // if the package belongs to the project, remove it if (JavaModelListener.sameElements(proj, element)) { PackageRefactorHandler.instance().handleRemove(root, (IPackageFragment) pack); } } } } /** * Handles package refactoring. * * @author bcmartin * @param <E> - The <code>IPackageFragment</code> implementation to use. */ class PackageRefactorHandler<E extends IPackageFragment> implements RefactorHandler<E> { private static final PackageRefactorHandler<IPackageFragment> INSTANCE = new PackageRefactorHandler<IPackageFragment>(); private PackageRefactorHandler() {} /** * @see edu.buffalo.cse.green.RefactorHandler#handleAdd(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleAdd(RootModel root, E element) { // do nothing } /** * @return The singleton instance of this <code>RefactorHandler</code>. */ public static RefactorHandler<IPackageFragment> instance() { return INSTANCE; } /** * @see edu.buffalo.cse.green.RefactorHandler#handleMove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement, org.eclipse.jdt.core.IJavaElement) */ public void handleMove(RootModel root, E sourceElement, E targetElement) { try { List<IJavaElement> cus = root.getElementsOfKind(COMPILATION_UNIT); ICompilationUnit[] newCUs = targetElement.getCompilationUnits(); for (IJavaElement cuElement : cus) { ICompilationUnit iCU = (ICompilationUnit) cuElement; if (JavaModelListener.sameElements( iCU.getAncestor(PACKAGE_FRAGMENT), sourceElement)) { for (ICompilationUnit cu : newCUs) { if (cu.getElementName().equals( iCU.getElementName())) { CompilationUnitRefactorHandler.instance( ).handleMove(root, iCU, cu); } } } } } catch (JavaModelException e) { e.printStackTrace(); } } /** * @see edu.buffalo.cse.green.RefactorHandler#handleRemove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleRemove(RootModel root, E element) { List<IJavaElement> cus = root.getElementsOfKind(COMPILATION_UNIT); for (IJavaElement cu : cus) { if (JavaModelListener.sameElements( cu.getAncestor(PACKAGE_FRAGMENT), element)) { CompilationUnitRefactorHandler.instance().handleRemove( root, (ICompilationUnit) cu); } } } } /** * Handles compilation unit refactoring. * * @author bcmartin * @param <E> - The <code>ICompilationUnit</code> implementation to use. */ class CompilationUnitRefactorHandler<E extends ICompilationUnit> implements RefactorHandler<E> { private static final CompilationUnitRefactorHandler<ICompilationUnit> INSTANCE = new CompilationUnitRefactorHandler<ICompilationUnit>(); /** * @return The singleton instance of this <code>RefactorHandler</code>. */ public static CompilationUnitRefactorHandler<ICompilationUnit> instance() { return INSTANCE; } /** * @see edu.buffalo.cse.green.RefactorHandler#handleAdd(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleAdd(RootModel root, E element) { // do nothing } /** * @see edu.buffalo.cse.green.RefactorHandler#handleMove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement, org.eclipse.jdt.core.IJavaElement) */ public void handleMove(RootModel root, E sourceElement, E targetElement) { // get all types represented in the diagram String scuId = sourceElement.getHandleIdentifier(); String tcuId = targetElement.getHandleIdentifier(); String scuName = scuId.substring(scuId.indexOf('{') + 1, scuId.indexOf(".java")); String tcuName = tcuId.substring(tcuId.indexOf('{') + 1, tcuId.indexOf(".java")); List<IJavaElement> cuTypes = root.getElementsOfKind(TYPE); // see if any types belong to the compilation unit // that is undergoing the move event for (IJavaElement oType : cuTypes) { if (JavaModelListener.sameElements(sourceElement, oType.getAncestor(COMPILATION_UNIT))) { String oId = oType.getHandleIdentifier(); String oName = oId.substring(oId.indexOf('[')); oName = oName.replaceAll("\\[" + scuName, "[" + tcuName); IJavaElement nType = JavaCore.create(tcuId + oName); TypeModel oModel = (TypeModel) root.getModelFromElement(oType); TypeModel nModel = root.createTypeModel((IType) nType); if (oModel != null) { // TODO We tried to catch a ResourceException, // but it is caught in MemberModel oModel.removeFromParent(); nModel.setLocation(oModel.getLocation()); nModel.setSize(oModel.getSize()); } } } } /** * @see edu.buffalo.cse.green.RefactorHandler#handleRemove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleRemove(RootModel root, E element) { // ignore this if the CU removed is a working copy (not actual removal) if (element.isWorkingCopy()) return; List<IJavaElement> types = new ArrayList<IJavaElement>( root.getElementsOfKind(TYPE)); // remove any types that belong to the compilation unit for (IJavaElement type : types) { if (JavaModelListener.sameElements(element, type.getAncestor(COMPILATION_UNIT))) { if (root.getModelFromElement(type) != null) { root.getModelFromElement(type).removeFromParent(); } } } } } /** * Handles type refactoring. * * @author bcmartin * @param <E> - The <code>IType</code> implementation to use. */ class TypeRefactorHandler<E extends IType> implements RefactorHandler<E> { private static final TypeRefactorHandler<IType> INSTANCE = new TypeRefactorHandler<IType>(); public static TypeModel REMOVED_TYPE; /** * @see edu.buffalo.cse.green.RefactorHandler#handleAdd(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleAdd(RootModel root, E element) { // if the compilation unit exists in the editor, add the type if (root.ancestorInEditor(element.getAncestor(COMPILATION_UNIT))) { TypeModel addedType = root.createTypeModel(element); if (REMOVED_TYPE != null) { addedType.setLocation(REMOVED_TYPE.getLocation()); addedType.setSize(REMOVED_TYPE.getSize()); } } } /** * @return The singleton instance of this <code>RefactorHandler</code>. */ public static TypeRefactorHandler<IType> instance() { return INSTANCE; } /** * @see edu.buffalo.cse.green.RefactorHandler#handleMove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement, org.eclipse.jdt.core.IJavaElement) */ public void handleMove(RootModel root, E sourceElement, E targetElement) { GreenException.illegalOperation("Unhandled element change"); } /** * @see edu.buffalo.cse.green.RefactorHandler#handleRemove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleRemove(RootModel root, E element) { REMOVED_TYPE = (TypeModel) root.getModelFromElement(element); } } /** * Handles field refactoring. * * @author bcmartin * @param <E> - The <code>IField</code> implementation to use. */ class FieldRefactorHandler<E extends IField> implements RefactorHandler<E> { private static final FieldRefactorHandler<IField> INSTANCE = new FieldRefactorHandler<IField>(); /** * @see edu.buffalo.cse.green.RefactorHandler#handleAdd(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleAdd(RootModel root, E element) { if (element.equals(element.getPrimaryElement())) { TypeModel type = (TypeModel) root.getModelFromElement( element.getAncestor(TYPE)); if (type != null) { type.addChild(new FieldModel(element)); } } } /** * @return The singleton instance of this <code>RefactorHandler</code>. */ public static FieldRefactorHandler<IField> instance() { return INSTANCE; } /** * @see edu.buffalo.cse.green.RefactorHandler#handleMove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement, org.eclipse.jdt.core.IJavaElement) */ public void handleMove(RootModel root, E sourceElement, E targetElement) { GreenException.illegalOperation("Unhandled element change"); } /** * @see edu.buffalo.cse.green.RefactorHandler#handleRemove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleRemove(RootModel root, E element) { FieldModel fieldModel = (FieldModel) root.getModelFromElement(element); // remove only stored copy if (fieldModel != null) { fieldModel.removeFromParent(); } } } /** * Handles method refactoring. * * @author bcmartin * @param <E> - The <code>IMethod</code> implementation to use. */ class MethodRefactorHandler<E extends IMethod> implements RefactorHandler<E> { private static final MethodRefactorHandler<IMethod> INSTANCE = new MethodRefactorHandler<IMethod>(); /** * @see edu.buffalo.cse.green.RefactorHandler#handleAdd(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleAdd(RootModel root, E element) { TypeModel type = (TypeModel) root.getModelFromElement( element.getAncestor(TYPE)); if (type != null) { type.addChild(new MethodModel((IMethod) element)); } } /** * @return The singleton instance of this <code>RefactorHandler</code>. */ public static MethodRefactorHandler<IMethod> instance() { return INSTANCE; } /** * @see edu.buffalo.cse.green.RefactorHandler#handleMove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement, org.eclipse.jdt.core.IJavaElement) */ public void handleMove(RootModel root, E sourceElement, E targetElement) { GreenException.illegalOperation("Unhandled element change"); } /** * @see edu.buffalo.cse.green.RefactorHandler#handleRemove(edu.buffalo.cse.green.editor.model.RootModel, org.eclipse.jdt.core.IJavaElement) */ public void handleRemove(RootModel root, E element) { MethodModel methodModel = (MethodModel) root.getModelFromElement(element); // remove only stored copy if (methodModel != null) { methodModel.removeFromParent(); } } }