| /******************************************************************************* |
| * Copyright (c) 2010 SpringSource 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: |
| * Kris De Volder - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.ajdt.internal.ui.refactoring.pullout; |
| |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.ajdt.core.javaelements.AJCompilationUnit; |
| import org.eclipse.ajdt.core.javaelements.AspectElement; |
| import org.eclipse.ajdt.core.javaelements.IntertypeElement; |
| import org.eclipse.ajdt.core.javaelements.SourceRange; |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.SubProgressMonitor; |
| import org.eclipse.jdt.core.IBuffer; |
| import org.eclipse.jdt.core.ICompilationUnit; |
| import org.eclipse.jdt.core.IJavaElement; |
| import org.eclipse.jdt.core.IJavaProject; |
| import org.eclipse.jdt.core.IMember; |
| import org.eclipse.jdt.core.IMethod; |
| import org.eclipse.jdt.core.IPackageFragment; |
| import org.eclipse.jdt.core.ISourceRange; |
| import org.eclipse.jdt.core.IType; |
| import org.eclipse.jdt.core.ITypeParameter; |
| import org.eclipse.jdt.core.ITypeRoot; |
| import org.eclipse.jdt.core.JavaCore; |
| import org.eclipse.jdt.core.JavaModelException; |
| import org.eclipse.jdt.core.dom.AST; |
| import org.eclipse.jdt.core.dom.ASTNode; |
| import org.eclipse.jdt.core.dom.ASTParser; |
| import org.eclipse.jdt.core.dom.ASTVisitor; |
| import org.eclipse.jdt.core.dom.Block; |
| import org.eclipse.jdt.core.dom.BodyDeclaration; |
| import org.eclipse.jdt.core.dom.ConstructorInvocation; |
| import org.eclipse.jdt.core.dom.FieldDeclaration; |
| import org.eclipse.jdt.core.dom.IBinding; |
| import org.eclipse.jdt.core.dom.IMethodBinding; |
| import org.eclipse.jdt.core.dom.ITypeBinding; |
| import org.eclipse.jdt.core.dom.IVariableBinding; |
| import org.eclipse.jdt.core.dom.MethodDeclaration; |
| import org.eclipse.jdt.core.dom.Modifier; |
| import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword; |
| import org.eclipse.jdt.core.dom.Name; |
| import org.eclipse.jdt.core.dom.NodeFinder; |
| import org.eclipse.jdt.core.dom.SimpleName; |
| import org.eclipse.jdt.core.dom.Statement; |
| import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; |
| import org.eclipse.jdt.core.manipulation.ImportReferencesCollector; |
| import org.eclipse.jdt.core.refactoring.CompilationUnitChange; |
| import org.eclipse.jdt.core.search.IJavaSearchConstants; |
| import org.eclipse.jdt.core.search.IJavaSearchScope; |
| import org.eclipse.jdt.core.search.SearchEngine; |
| import org.eclipse.jdt.core.search.SearchMatch; |
| import org.eclipse.jdt.core.search.SearchParticipant; |
| import org.eclipse.jdt.core.search.SearchPattern; |
| import org.eclipse.jdt.core.search.SearchRequestor; |
| import org.eclipse.jdt.internal.corext.refactoring.base.JavaStatusContext; |
| import org.eclipse.jdt.internal.corext.util.CodeFormatterUtil; |
| import org.eclipse.jdt.internal.corext.util.JdtFlags; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.Document; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.Region; |
| import org.eclipse.ltk.core.refactoring.Change; |
| import org.eclipse.ltk.core.refactoring.CompositeChange; |
| import org.eclipse.ltk.core.refactoring.Refactoring; |
| import org.eclipse.ltk.core.refactoring.RefactoringStatus; |
| import org.eclipse.ltk.core.refactoring.RefactoringStatusContext; |
| import org.eclipse.text.edits.DeleteEdit; |
| import org.eclipse.text.edits.InsertEdit; |
| import org.eclipse.text.edits.MalformedTreeException; |
| import org.eclipse.text.edits.MultiTextEdit; |
| import org.eclipse.text.edits.ReplaceEdit; |
| import org.eclipse.text.edits.TextEdit; |
| |
| public class PullOutRefactoring extends Refactoring { |
| |
| /** |
| * An instance of this class pulls together all required info for |
| * the rewriting of the aspect and handles the rewriting. |
| */ |
| class AspectRewrite { |
| |
| /** |
| * Collects the text of all ITDs. |
| */ |
| private StringBuffer itds = new StringBuffer(); |
| |
| /** |
| * Collects info for and handles rewriting of imports for the target aspect. |
| */ |
| private ImportRewrite importRewrite; |
| |
| /** |
| * This field may be set while creating the "deletion" edits to pull out ITDs. |
| * This will only happen if the compilation unit from which we are pulling out |
| * is the same as the target aspect's compilation unit. If set to a non-null |
| * value this cuChange object should be used for recording the "insertion" |
| * edits. |
| */ |
| private CompilationUnitChange cuChange = null; |
| |
| public AspectRewrite() throws JavaModelException { |
| importRewrite = ImportRewrite.create(targetAspect.getCompilationUnit(), true); |
| } |
| |
| public void addITD(ITDCreator itd, RefactoringStatus status) throws JavaModelException, BadLocationException { |
| itd.collectImports(importRewrite, status); |
| itds.append(itd.createText()); |
| } |
| |
| |
| /** |
| * Create an ICompiltationUnitChange with all changes to the aspect's CU, related to the insertion |
| * of ITDs. Ensure these changes are added to the allChanges object. |
| */ |
| private void rewriteAspect(IProgressMonitor submonitor, CompositeChange allChanges) { |
| try { |
| CompilationUnitChange edits = getCUChange(); |
| |
| // Create edit to add "privileged" |
| if (isMakePrivileged() && !isPrivileged()) { |
| int start = targetAspect.getSourceRange().getOffset(); |
| int nameStart = targetAspect.getNameRange().getOffset() - start; |
| String aspectText = targetAspect.getSource().substring(0,nameStart); |
| // If all is well, we now have the aspectText, upto the keywords "aspect" |
| // Caveat: for some reason AJDT has replaced this keyword by the "class" keyword. |
| int aspectKeywordStart = aspectText.lastIndexOf("class"); |
| Assert.isTrue(aspectKeywordStart>=0, "The aspect keyword was not found in the aspect source"); |
| aspectKeywordStart += start; // Adjust because the start of our string may not be the |
| // start of the compilation unit. |
| edits.addEdit(new InsertEdit(aspectKeywordStart, "privileged ")); |
| } |
| |
| // Create edit to the imports section of compilation unit |
| if (importRewrite.hasRecordedChanges()) { |
| try { |
| edits.addEdit(importRewrite.rewriteImports(submonitor)); |
| } catch (Exception e) { |
| //An aspect handles this |
| } |
| } |
| |
| // Add the itds to the aspect |
| edits.addEdit(new InsertEdit(getInsertLocation(), itds.toString())); |
| |
| if (edits.getParent()==null) { |
| allChanges.add(edits); |
| } |
| else { |
| //If not null, it means we already added it, because the aspect is |
| //in the same CU as some pulled out members. |
| } |
| } catch (JavaModelException e) { |
| } |
| } |
| |
| /** |
| * Get the CompilationUnitChange object that should be used to record the changes related to |
| * inserting ITDs into the aspect. The object is created if necessary, or reused if it |
| * already exists. |
| */ |
| private CompilationUnitChange getCUChange() { |
| if (cuChange==null) { |
| cuChange = newCompilationUnitChange(getAspect().getCompilationUnit()); |
| cuChange.setEdit(new MultiTextEdit()); // root element must be set, or we can't add edits! |
| } |
| return cuChange; |
| } |
| |
| public void setCUChange(CompilationUnitChange cuChange) { |
| this.cuChange = cuChange; |
| } |
| |
| } |
| |
| /** |
| * Helper class to create ITD text from a member. An instance of this class is created |
| * to provide a working area in which to build the ITD text. |
| * <p> |
| * This class was introduced to help manage the complexity of context information that |
| * was getting passed along with various helper methods that break down the creation of an ITD |
| * into smaller steps. This was starting to result in extremely long argument lists. |
| * <p> |
| * This class keeps all that information in one convenient place. It also provides a nice |
| * high-level interface to manipulate the properties of the created ITD, while encapsulating |
| * the messier parts of the rewriting and text manipulation code inside the class. |
| * |
| * @author kdvolder |
| */ |
| static class ITDCreator { |
| |
| private static final int VISIBILITY_MODIFIERS = Modifier.PRIVATE | Modifier.PUBLIC | Modifier.PROTECTED; |
| |
| /** |
| * The original member from which we create the ITD |
| */ |
| private IMember member; |
| |
| /** |
| * The AST node corresponding to the original member. |
| */ |
| private BodyDeclaration memberNode; |
| |
| /** |
| * We place the text of the original member in this document, so that |
| * we can accumulate and apply edits against the document to create |
| * the final ITD text. |
| */ |
| private IDocument memberText; |
| |
| /** |
| * Rather than applying edits immeditiately, we accumulate them in here, this is so that |
| * we don't end up destroying position information before we have figured out |
| * all the edits to apply to the original text. |
| */ |
| private MultiTextEdit edits = new MultiTextEdit(); |
| |
| /** |
| * Modifiers that should be removed when we rewrite the ITD's modifiers. |
| * This is a "bitfield". See {@link Modifier} for the meaning of the bits. |
| */ |
| private int deleteMods = 0; |
| |
| /** |
| * Either an empty String, or a String containing all the modifiers to add, separated |
| * by spaces and with one trailing space. |
| */ |
| private String insertMods = ""; |
| |
| /** |
| * The name of the declaring type, as it should be written in the aspect context (i.e. as |
| * a simple name, or a fully qualified name, depending on whether it could be imported. |
| * <p> |
| * This field is initialized during "collectImports". |
| */ |
| private String declaringTypeRef = null; |
| |
| /** |
| * The ITD creator requires an IMember and its corresponding AST node, to be |
| * able to perform its work of creating the ITD text. |
| * |
| * @param member |
| * @param memberNode |
| * @throws JavaModelException |
| */ |
| public ITDCreator(IMember member, BodyDeclaration memberNode) throws JavaModelException { |
| this.member = member; |
| this.memberNode = memberNode; |
| this.memberText = new Document(getAJSource(member)); |
| } |
| |
| /** |
| * Aspect aware method for getting source code. For elements inside an .aj file, it fetches the |
| * "original" source code, not the rewritten source code. For elements in regular .java file |
| * it is identical to the getSource method. |
| */ |
| private String getAJSource(IMember member) throws JavaModelException { |
| ICompilationUnit cu = member.getCompilationUnit(); |
| if (cu instanceof AJCompilationUnit) { |
| AJCompilationUnit ajcu = (AJCompilationUnit) cu; |
| ajcu.requestOriginalContentMode(); |
| try { |
| return member.getSource(); |
| } |
| finally { |
| ajcu.discardOriginalContentMode(); |
| } |
| } |
| return member.getSource(); |
| } |
| |
| @Override |
| public String toString() { |
| if (memberText==null) |
| return "ITDCreator(DISPOSED)"; |
| else { |
| return "ITDCreator(----\n" + |
| memberText.get()+"\n" + |
| "----)"; |
| } |
| } |
| |
| /** |
| * Collect imports needed for this ITD, and add them to the aspects compilation unit's |
| * importRewriter. |
| * <p> |
| * This also applies the necessary edits to the ITD text if some imports fail because |
| * of name clashes. |
| * <p> |
| * If some references can't be resolved a warning is added to the refactoring satus. |
| */ |
| public void collectImports(ImportRewrite importRewrite, RefactoringStatus status) throws JavaModelException { |
| Region rangeLimit = new Region(memberNode.getStartPosition(), memberNode.getLength()); |
| |
| List<SimpleName> extraType = new ArrayList<SimpleName>(); |
| List<SimpleName> extraStatic = new ArrayList<SimpleName>(); |
| |
| ImportReferencesCollector.collect(memberNode, member.getJavaProject(), rangeLimit, extraType, extraStatic); |
| |
| for (Name name : extraStatic) { |
| IBinding binding = name.resolveBinding(); |
| if (binding==null) { |
| status.addWarning("Couldn't resolve binding, imports may be incorrect", PullOutRefactoring.makeContext(member, name)); |
| } |
| else { |
| replaceNameRef(name, importRewrite.addStaticImport(binding)); |
| } |
| } |
| |
| for (Name name : extraType) { |
| ITypeBinding binding = (ITypeBinding)name.resolveBinding(); |
| if (binding==null) { |
| status.addWarning("Couldn't resolve binding, imports may be incorrect", PullOutRefactoring.makeContext(member, name)); |
| } |
| else { |
| if (binding.isParameterizedType()) { |
| // Simple names should not be treated as complete generic type references |
| binding = binding.getErasure(); |
| } |
| replaceNameRef(name, importRewrite.addImport(binding)); |
| } |
| } |
| |
| if (wasIntertype()) { |
| IntertypeElement ite = (IntertypeElement) member; |
| AJCompilationUnit cu = (AJCompilationUnit) ite.getCompilationUnit(); |
| IType targetType = ite.findTargetType(); |
| String typeQName = targetType!=null?targetType.getFullyQualifiedName():ite.getTargetName(); |
| declaringTypeRef = importRewrite.addImport(typeQName); |
| } |
| else { |
| declaringTypeRef = importRewrite.addImport(member.getDeclaringType().getFullyQualifiedName()); |
| } |
| } |
| |
| /** |
| * @return true if the original pulled out member was *already* an ITD. |
| */ |
| private boolean wasIntertype() { |
| return member instanceof IntertypeElement; |
| } |
| |
| /** |
| * Add an extra textedit, if needed for updating a potentially failed import rewrite |
| */ |
| private void replaceNameRef(Name name, String replaceStr) throws MalformedTreeException, JavaModelException { |
| String orgRefText = name.getFullyQualifiedName(); |
| if (replaceStr.equals(orgRefText)) |
| return; |
| edits.addChild(new ReplaceEdit( |
| name.getStartPosition()-memberStart(), |
| name.getLength(), |
| replaceStr)); |
| } |
| |
| |
| /** |
| * Create the necessary edits to change modifiers on the ITD as described by the |
| * deleteMods and insertMods fields. |
| */ |
| private void rewriteModifiers() |
| throws BadLocationException, MalformedTreeException, JavaModelException |
| { |
| if (deleteMods!=0) { |
| List<Modifier> mods = memberNode.modifiers(); |
| //String replaceText = makePublic?"public ":""; |
| String replaceText = ""; |
| for (Modifier modifier : mods) { |
| if ( (modifier.getKeyword().toFlagValue() & deleteMods) != 0) { |
| int modStart = modifier.getStartPosition() - memberStart(); |
| int modEnd = modStart + modifier.getLength(); |
| if (Character.isWhitespace(memberText.getChar(modEnd))) { |
| // Delete one extra white space character for a nicer look. |
| // but only if there is one! (imagine there being a weird comment there instead) |
| modEnd++; |
| } |
| edits.addChild(new ReplaceEdit(modStart, modEnd-modStart, replaceText)); |
| } |
| } |
| } |
| if (insertMods!=null && !"".equals(insertMods)) { |
| int insertPos; |
| if (memberNode instanceof MethodDeclaration) { |
| MethodDeclaration methodNode = (MethodDeclaration) memberNode; |
| if (methodNode.isConstructor()) |
| insertPos = methodNode.getName().getStartPosition() - memberStart(); |
| else |
| insertPos = methodNode.getReturnType2().getStartPosition() - memberStart(); |
| } |
| else if (memberNode instanceof FieldDeclaration ) { |
| FieldDeclaration fieldNode = (FieldDeclaration) memberNode; |
| insertPos = fieldNode.getType().getStartPosition() - memberStart(); |
| } |
| else { |
| insertPos = 0; |
| } |
| edits.addChild(new InsertEdit(insertPos, insertMods)); |
| } |
| } |
| |
| private int memberStart() throws JavaModelException { |
| return member.getSourceRange().getOffset(); |
| } |
| |
| public void removeModifier(int mod) { |
| this.deleteMods |= mod; |
| } |
| |
| /** |
| * Produce text for the ITD, by applying all requested changes to the |
| * original ITD text. |
| * <p> |
| * This is a 'once only' operation. After the text of the ITD is created |
| * this object has served its purpose and should not be used anymore. |
| * @throws JavaModelException |
| * @throws BadLocationException |
| * @throws MalformedTreeException |
| */ |
| public String createText() throws JavaModelException, MalformedTreeException, BadLocationException { |
| try { |
| int memberStart = memberStart(); |
| |
| // All positions, except for memberStart itself, will be computed relative to member |
| // start (since our memberText document only contains the member text) |
| |
| int memberEnd = memberText.getLength(); |
| int nameStart = member.getNameRange().getOffset() - memberStart; |
| |
| // Add some indentation correction to the front |
| edits.addChild( |
| new InsertEdit(0, CodeFormatterUtil.createIndentString(1, member.getJavaProject()))); |
| |
| // Rewrite modifiers |
| rewriteModifiers(); |
| |
| //Rewrite stuff in and around the name... only when original is *not* an intertype element! |
| if (!wasIntertype()) { |
| // Insert declaring type reference in front of name |
| IType declaringType = member.getDeclaringType(); |
| Assert.isNotNull(declaringTypeRef, "The declaring type name is computed by collectImports. Forgot to call it?"); |
| StringBuffer typeName = new StringBuffer(declaringTypeRef); |
| ITypeParameter[] typeParameters = declaringType.getTypeParameters(); |
| if (typeParameters !=null && typeParameters.length>0) { |
| typeName.append("<"); |
| for (int i = 0; i < typeParameters.length; i++) { |
| if (i>0) typeName.append(", "); |
| typeName.append(typeParameters[i].getElementName()); |
| } |
| typeName.append(">"); |
| } |
| typeName.append( "." ); |
| edits.addChild(new InsertEdit(nameStart, typeName.toString())); |
| |
| // For constructors, must change name to "new" |
| if (member instanceof IMethod && ((IMethod)member).isConstructor()) { |
| edits.addChild(new ReplaceEdit(nameStart, member.getNameRange().getLength(), "new")); |
| } |
| } |
| |
| // Add some newlines to the end for nicer spacing |
| String newline = memberText.getLineDelimiter(0); |
| if (newline==null) // We tried to use the same as in the memberText but it has none |
| newline = System.getProperty("line.separator"); |
| edits.addChild(new InsertEdit(memberEnd, newline+newline)); |
| |
| // applying these edits should produce the ITD text |
| edits.apply(memberText, TextEdit.NONE); |
| return memberText.get(); |
| } |
| finally { |
| // This object has served it's purpose and should not be reused. |
| dispose(); |
| } |
| } |
| |
| /** |
| * Destroy this object. Further use of the object will probably cause NPE exception. |
| */ |
| private void dispose() { |
| member = null; |
| memberNode = null; |
| memberText = null; |
| edits = null; |
| insertMods = null; |
| declaringTypeRef = null; |
| } |
| |
| /** |
| * Was the original member protected. |
| */ |
| public boolean wasProtected() throws JavaModelException { |
| return JdtFlags.isProtected(member); |
| } |
| |
| /** |
| * Was the original member public. |
| */ |
| public boolean wasPublic() throws JavaModelException { |
| return JdtFlags.isPublic(member); |
| } |
| |
| /** |
| * Was the original member private. |
| */ |
| public boolean wasPrivate() throws JavaModelException { |
| return JdtFlags.isPrivate(member); |
| } |
| |
| /** |
| * Was the original member abstract. |
| */ |
| public boolean wasAbstract() throws JavaModelException { |
| return JdtFlags.isAbstract(member); |
| } |
| |
| /** |
| * Was the original member "package visible" (i.e. it has no visibility |
| * modifiers at all. |
| */ |
| public boolean wasPackageVisible() throws JavaModelException { |
| return JdtFlags.isPackageVisible(member); |
| } |
| |
| /** |
| * Get the original member from which we are creating an ITD. |
| */ |
| public IMember getMember() { |
| return member; |
| } |
| |
| /** |
| * Get the IJavaElement name of the original member. |
| */ |
| public String getElementName() { |
| return member.getElementName(); |
| } |
| |
| public ASTNode getMemberNode() { |
| return memberNode; |
| } |
| |
| public void addModifier(int modFlag) { |
| if (isVisibilityModifier(modFlag)) { |
| //When adding a visiblity modifier, make sure to remove any preexisting |
| //visibility modifiers first |
| deleteMods |= VISIBILITY_MODIFIERS; |
| } |
| ModifierKeyword toAdd = ModifierKeyword.fromFlagValue(modFlag); |
| String toAddStr = toAdd.toString(); |
| removeModifier(modFlag); |
| if (!insertMods.contains(toAddStr)) { |
| insertMods += toAddStr+" "; |
| } |
| } |
| |
| private boolean isVisibilityModifier(int modFlag) { |
| return (modFlag & VISIBILITY_MODIFIERS)!=0; |
| } |
| |
| /** |
| * Replace the body of the method with given text. This is really only |
| * supposed to be used to add method stubs for methods that where abstract |
| * before. |
| */ |
| public void setBody(String bodyText) { |
| Object bodyNode = memberNode.getStructuralProperty(MethodDeclaration.BODY_PROPERTY); |
| Assert.isTrue(bodyNode==null, "There already is a method body for this member: "+getMember()); |
| int startPos = memberText.get().lastIndexOf(';'); |
| edits.addChild(new ReplaceEdit(startPos, 1, bodyText)); |
| } |
| |
| /** |
| * Is the original member a constructor (does not include the case where the original member is |
| * already an ITD, even if that ITD introduces a constructor). |
| */ |
| public boolean wasConstructorMethod() throws JavaModelException { |
| return (member instanceof IMethod) && !wasIntertype() && ((IMethod)member).isConstructor(); |
| } |
| |
| /** |
| * Determine whether this ITD's original member (which is assumed to be a constructor) has a |
| * call to 'this()' |
| */ |
| public boolean hasThisCall() throws JavaModelException { |
| Assert.isNotNull(memberNode); |
| Assert.isLegal(wasConstructorMethod()); |
| Block body = ((MethodDeclaration)memberNode).getBody(); |
| if (body==null) return false; |
| @SuppressWarnings("unchecked") List<Statement> stms = body.statements(); |
| if (stms==null || stms.size()==0) return false; |
| Statement firstStm = stms.get(0); |
| if (!(firstStm instanceof ConstructorInvocation)) return false; |
| ConstructorInvocation call = (ConstructorInvocation) firstStm; |
| // The class ConstructorInvocation only represents "this(...)" calls |
| return call.arguments().isEmpty(); |
| } |
| } |
| |
| private static final String MAKE_PRIVILEGED = "makePrivileged"; |
| private static final String MEMBER = "member"; |
| |
| protected static final String ASPECT = "aspect"; |
| |
| /** |
| * The members to pull out, grouped by compilation unit for efficiency sake ( |
| * so we can process them one CU at a time) |
| */ |
| private Map<ICompilationUnit, Collection<IMember>> memberMap; |
| private HashSet<IMember> memberSet; |
| |
| /** |
| * The target aspect to where the method should be moved. |
| */ |
| private AspectElement targetAspect; |
| |
| /** |
| * Should we make the aspect privileged |
| */ |
| private boolean makePrivileged = false; |
| |
| /** |
| * Allow pulling abstract methods. This deletes abstract keyword and generates |
| * method stubs for "abstract" ITDs. |
| */ |
| private boolean generateAbstractMethodStubs = false; |
| |
| /** |
| * Allow the deletion of the protected keyword from ITDs (because this keyword |
| * is not allowed on ITDs by AspectJ). |
| */ |
| private boolean allowDeleteProtected = false; |
| |
| /** |
| * Allow to make ITDs public to avoid breaking references to pulled members. |
| */ |
| private boolean allowMakePublic; |
| |
| private IJavaProject javaProject; |
| private AspectRewrite aspectChanges; |
| |
| public PullOutRefactoring() { |
| clearMembers(); // initializes the member map and sets |
| } |
| |
| public void addMember(IMember member, RefactoringStatus status) { |
| ICompilationUnit cu = member.getCompilationUnit(); |
| Collection<IMember> members = getMembers(cu); |
| members.add(member); |
| memberSet.add(member); |
| if (javaProject==null) |
| javaProject = member.getJavaProject(); |
| else if (javaProject!=member.getJavaProject()) |
| status.addError("Pull-out refactoring across multiple projects is not suppored", makeContext(member)); |
| } |
| |
| @Override |
| public RefactoringStatus checkFinalConditions(IProgressMonitor pm) |
| throws CoreException, OperationCanceledException { |
| RefactoringStatus status = new RefactoringStatus(); |
| SubProgressMonitor submonitor = new SubProgressMonitor(pm, memberMap.keySet().size()); |
| submonitor.beginTask("Checking preconditions...", memberMap.keySet().size()); |
| try { |
| aspectChanges = new AspectRewrite(); |
| // For a more predictable and orderly outcome, sort by the name of the CU |
| ICompilationUnit[] cus = memberMap.keySet().toArray(new ICompilationUnit[0]); |
| Arrays.sort(cus, CompilationUnitComparator.the); |
| for (ICompilationUnit cu : cus) { |
| ASTParser parser= ASTParser.newParser(AST.JLS8); |
| parser.setSource(cu); |
| parser.setResolveBindings(true); |
| ASTNode cuNode = parser.createAST(pm); |
| for (IMember member : memberMap.get(cu)) { |
| BodyDeclaration memberNode = (BodyDeclaration) findASTNode(cuNode, member); |
| ITDCreator itd = new ITDCreator(member, memberNode); |
| if (member.getDeclaringType().isInterface()) { |
| // No need to check "isAllowMakePublic" since technically it was already public. |
| itd.addModifier(Modifier.PUBLIC); |
| } |
| if (itd.wasProtected()) { |
| if (isAllowDeleteProtected()) { |
| itd.removeModifier(Modifier.PROTECTED); |
| } |
| else { |
| status.addWarning("moved member '"+member.getElementName()+"' is protected\n" + |
| "protected ITDs are not allowed by AspectJ.\n" , |
| makeContext(member)); |
| } |
| } |
| if (itd.wasAbstract()) { |
| if (isGenerateAbstractMethodStubs()) { |
| itd.removeModifier(Modifier.ABSTRACT); |
| itd.setBody(getAbstractMethodStubBody(member)); |
| } |
| else { |
| status.addWarning("moved member '"+member.getElementName()+"' is abstract.\n" + |
| "abstract ITDs are not allowed by AspectJ.\n" + |
| "You can enable the 'convert abstract methods' option to avoid this error.", |
| makeContext(member)); |
| //If you choose to ignore this error and perform refactoring anyway... |
| // We make sure the abstract keyword is added to the itd, so you will get a compile error |
| // and be forced to deal with that error. |
| itd.addModifier(Modifier.ABSTRACT); |
| } |
| } |
| checkOutgoingReferences(itd, status); |
| checkIncomingReferences(itd, status); |
| checkConctructorThisCall(itd, status); |
| aspectChanges.addITD(itd, status); |
| } |
| submonitor.worked(1); |
| } |
| } catch (BadLocationException e) { |
| status.merge(RefactoringStatus.createFatalErrorStatus("Internal error:"+e.getMessage())); |
| } |
| finally { |
| submonitor.done(); |
| } |
| return status; |
| } |
| |
| /** |
| * Check whether the constructor is "safe" to pull out, or whether it might change |
| * the meaning of the program (no longer executing initialiser code in target class). |
| * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=318936 |
| */ |
| private void checkConctructorThisCall(ITDCreator itd, |
| RefactoringStatus status) throws JavaModelException { |
| if (itd.wasConstructorMethod() && !itd.hasThisCall()) { |
| status.addWarning("Program semantics changed: moved '"+itd.getElementName()+"' constructor has no this() call. Initializers in the target class will not be executed " + |
| "by the intertype constructor", makeContext(itd.getMember())); |
| } |
| } |
| |
| /** |
| * Find AST node corresponding to a given IMember. |
| */ |
| private ASTNode findASTNode(ASTNode cuNode, IMember member) |
| throws JavaModelException { |
| ISourceRange range = member.getSourceRange(); |
| NodeFinder finder = new NodeFinder(cuNode, range.getOffset(), range.getLength()); |
| return finder.getCoveredNode(); |
| // Note: why we *have* to use getCoveredNode explicitly rather than use the |
| // perform methods defined on NodeFinder. |
| // See BUG 316945: Normally, we have exact positions and covering/covered are the same node. |
| // but in the BUG case we should use the covered node since a JDT bug makes the source range |
| // be too large. |
| } |
| |
| /** |
| * Check whether references to moved elements become broken. Update status message |
| * accordingly (but only if allowModifierConversion is set to false). |
| * |
| * @return true if no references become broken |
| */ |
| private boolean checkIncomingReferences(ITDCreator movedMember, RefactoringStatus status) throws CoreException { |
| if (movedMember.wasPublic()) |
| return true; //Always ok if member was already public |
| boolean ok = true; |
| IJavaSearchScope scope= SearchEngine.createJavaSearchScope(new IJavaElement[] { javaProject }); |
| SearchPattern pattern= SearchPattern.createPattern(movedMember.getMember(), IJavaSearchConstants.REFERENCES); |
| SearchEngine engine= new SearchEngine(); |
| final Set<SearchMatch> references = new HashSet<SearchMatch>(); |
| engine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant()}, scope, new SearchRequestor() { |
| @Override |
| public void acceptSearchMatch(SearchMatch match) throws CoreException { |
| if (match.getAccuracy() == SearchMatch.A_ACCURATE && !match.isInsideDocComment()) |
| references.add(match); |
| } |
| }, new NullProgressMonitor()); |
| |
| String referredPkg = getPackageName(targetAspect); // since the element is moved it's package *will* be... |
| for (SearchMatch match : references) { |
| if (match.getElement() instanceof IJavaElement) { |
| IJavaElement referingElement = (IJavaElement) match.getElement(); |
| if (!isMoved(referingElement)) { |
| if (movedMember.wasPrivate()) { |
| ok = false; |
| if (isAllowMakePublic()) { |
| movedMember.addModifier(Modifier.PUBLIC); |
| } |
| else { |
| status.addWarning("The moved private member '"+movedMember.getElementName()+"' will not be accessible" + |
| " after refactoring.", |
| makeContext(match)); |
| } |
| } |
| else if (movedMember.wasPackageVisible() || movedMember.wasProtected()) { |
| String referringPkg = getPackageName(referingElement); |
| if (referringPkg!=null && !referringPkg.equals(referredPkg)) { |
| ok = false; |
| if (isAllowMakePublic()) { |
| movedMember.addModifier(Modifier.PUBLIC); |
| } |
| else { |
| status.addWarning("The moved member '"+movedMember.getElementName()+"' may not be accessible " + |
| "after refactoring", |
| makeContext(match)); |
| } |
| } |
| } |
| } |
| } |
| } |
| return ok; |
| } |
| |
| /** |
| * Retrieve package name of a IJavaElement. |
| * @return The name of the package, or null if the IJavaElement is not nested inside a IPackagFragment. |
| */ |
| private String getPackageName(IJavaElement el) { |
| IPackageFragment pkg = (IPackageFragment) el.getAncestor(IJavaElement.PACKAGE_FRAGMENT); |
| if (pkg==null) return null; |
| return pkg.getElementName(); |
| } |
| |
| @Override |
| public RefactoringStatus checkInitialConditions(IProgressMonitor monitor) |
| throws CoreException, OperationCanceledException { |
| |
| RefactoringStatus status= new RefactoringStatus(); |
| monitor.beginTask("Checking preconditions...", 1); |
| try { |
| if (memberMap == null || memberMap.isEmpty()) |
| status.merge(RefactoringStatus.createFatalErrorStatus("No pullout targets have been specified.")); |
| else { |
| for (ICompilationUnit cu : memberMap.keySet()) { |
| for (IMember member : memberMap.get(cu)) { |
| if (!member.exists()) { |
| status.merge(RefactoringStatus.createFatalErrorStatus( |
| MessageFormat.format("Member ''{0}'' does not exist.", |
| new Object[] { member.getElementName()}))); |
| } |
| else if (!isInTopLevelType(member)) { |
| status.merge(RefactoringStatus.createFatalErrorStatus( |
| MessageFormat.format("Member ''{0}'' is not directly nested in a top-level type.", |
| new Object[] { member.getElementName()}))); |
| } |
| else if (member.isBinary()) { |
| status.merge(RefactoringStatus.createFatalErrorStatus( |
| MessageFormat.format("Member ''{0}'' is not in source code. Binary methods can not be refactored.", |
| new Object[] { member.getElementName()}))); |
| } |
| else if (!member.getCompilationUnit().isStructureKnown()) { |
| status.merge(RefactoringStatus.createFatalErrorStatus( |
| MessageFormat.format("Compilation unit ''{0}'' contains compile errors.", |
| new Object[] { cu.getElementName()}))); |
| } |
| } |
| } |
| } |
| } finally { |
| monitor.done(); |
| } |
| return status; |
| } |
| |
| private void checkOutgoingReferences(final ITDCreator itd, final RefactoringStatus status) |
| throws CoreException, OperationCanceledException { |
| if (willBePrivileged()) |
| return; //Always OK! |
| |
| // Walk the AST to find problematic references (e.g. references to private members from within the moved method) |
| itd.getMemberNode().accept(new ASTVisitor() { |
| /** |
| * Check for various problems that may be caused by moving a reference into |
| * a different context. |
| */ |
| private void checkReference(ASTNode node, final IBinding binding, RefactoringStatus status) { |
| if (isField(binding) || isMethod(binding) || isType(binding)) { |
| if (isTypeParameter(binding)) |
| return; //Exclude these, or they'll look like package restricted types in code below. |
| if (isMoved(binding)) |
| return; //OK: anything moved to the aspect will be accessible from the aspect |
| int mods = binding.getModifiers(); |
| if (Modifier.isPrivate(mods)) { |
| status.addWarning("private member '"+binding.getName()+"' accessed and refactored aspect is not privileged", |
| makeContext(itd.getMember(), node)); |
| } |
| if (JdtFlags.isProtected(binding) || JdtFlags.isPackageVisible(binding)) { |
| // FIXKDV: separate case for protected |
| // These are really two separate cases, but the cases where this matters (i.e. |
| // aspects that have a super type are rare so I'm not dealing with that |
| // right now (this is relatively harmless: will result in a spurious warning message |
| // in rare case where the pulled member is protected and is pulled from target aspect's |
| // supertype that is not in the same package as target aspect) |
| String referredPkg = getPackageName(binding.getJavaElement()); |
| if (referredPkg!=null) { |
| //If it has no package, we'll just ignore it, whatever it is, it's probably not subject to |
| // package scope :-) |
| String aspectPkg = targetAspect.getPackageFragment().getElementName(); |
| if (!referredPkg.equals(aspectPkg)) { |
| String keyword = JdtFlags.isProtected(binding)?"protected":"package restricted"; |
| status.addWarning(keyword+" member '"+binding.getName()+"' is accessed and refactored aspect is not privileged", |
| makeContext(itd.getMember(), node)); |
| } |
| } |
| } |
| } |
| } |
| |
| private boolean isField(IBinding binding) { |
| return (binding instanceof IVariableBinding) |
| && ((IVariableBinding)binding).isField(); |
| } |
| |
| private boolean isMethod(IBinding binding) { |
| return (binding instanceof IMethodBinding); |
| } |
| |
| private boolean isType(IBinding binding) { |
| return binding instanceof ITypeBinding; |
| } |
| |
| private boolean isTypeParameter(IBinding binding) { |
| return binding instanceof ITypeBinding |
| && ( ((ITypeBinding)binding).isCapture() |
| || ((ITypeBinding)binding).isTypeVariable() ); |
| } |
| |
| @Override |
| public boolean visit(SimpleName node) { |
| IBinding binding = node.resolveBinding(); |
| checkReference(node, binding, status); |
| return true; |
| } |
| |
| }); |
| } |
| |
| private void clearMembers() { |
| memberMap = new HashMap<ICompilationUnit, Collection<IMember>>(); |
| memberSet = new HashSet<IMember>(); |
| javaProject = null; |
| } |
| |
| @Override |
| public Change createChange(IProgressMonitor pm) throws CoreException, |
| OperationCanceledException { |
| try { |
| pm.beginTask("Creating changes...", memberMap.keySet().size()); |
| CompositeChange allChanges = new CompositeChange("PullOut ITDs"); |
| for (ICompilationUnit cu : memberMap.keySet()) { |
| |
| // Prepare an ASTRewriter for this compilation unit |
| ASTParser parser= ASTParser.newParser(AST.JLS8); |
| parser.setSource(cu); |
| ASTNode cuNode = parser.createAST(pm); |
| MultiTextEdit cuEdits = new MultiTextEdit(); |
| |
| // Apply all operations to the AST rewriter |
| for (IMember member : getMembers(cu)) { |
| ISourceRange range = member.getSourceRange(); |
| range = grabSpaceBefore(cu, range); |
| cuEdits.addChild(new DeleteEdit(range.getOffset(), range.getLength())); |
| } |
| |
| // Create CUChange object with the accumulated deletion edits. |
| CompilationUnitChange cuChanges = newCompilationUnitChange(cu); |
| cuChanges.setEdit(cuEdits); |
| |
| // Add changes for this compilation unit. |
| allChanges.add(cuChanges); |
| |
| pm.worked(1); |
| } |
| aspectChanges.rewriteAspect(pm, allChanges); |
| return allChanges; |
| } |
| finally { |
| pm.done(); |
| } |
| } |
| |
| /** |
| * For cosmetic reasons (nicer indentation of resulting text after deletion of membernode |
| * we force the nodes sourcerange to include any spaces in front of the node, upto the |
| * beginning of the line. |
| * @param cu |
| * @return |
| */ |
| private ISourceRange grabSpaceBefore(ICompilationUnit cu, ISourceRange range) { |
| try { |
| IBuffer sourceText = cu.getBuffer(); |
| int start = range.getOffset(); |
| int len = range.getLength(); |
| while (start>0 && isSpace(sourceText.getChar(start-1))) { |
| start--; len++; |
| } |
| return new SourceRange(start, len); |
| } catch (JavaModelException e) { |
| //This operation is not essential, so it is fine if it silently fails. |
| return range; |
| } |
| } |
| |
| private boolean isSpace(char c) { |
| return c==' '||c=='\t'; |
| } |
| |
| private CompilationUnitChange newCompilationUnitChange(ICompilationUnit cu) { |
| CompilationUnitChange cuChange = new CompilationUnitChange("PullOut ITDs", cu); |
| if (targetAspect.getCompilationUnit()==cu) { |
| //Also use this cuChange object for the aspect changes |
| aspectChanges.setCUChange(cuChange); |
| } |
| return cuChange; |
| } |
| |
| /** |
| * @return The target aspect where we will create the intertype declarations. |
| */ |
| public AspectElement getAspect() { |
| return targetAspect; |
| } |
| |
| public String getAspectName() { |
| AspectElement theAspect = getAspect(); |
| if (theAspect==null) return ""; |
| return theAspect.getFullyQualifiedName(); |
| } |
| |
| /** |
| * Compute the location where new ITDs will be inserted in the target aspect's source code. |
| */ |
| private int getInsertLocation() { |
| try { |
| return targetAspect.getSourceRange().getOffset() |
| + targetAspect.getSourceRange().getLength()-1; |
| } catch (JavaModelException e) { |
| return 0; |
| } |
| } |
| |
| IJavaProject getJavaProject() { |
| return javaProject; |
| } |
| |
| public IMember[] getMembers() { |
| List<IMember> members = new ArrayList<IMember>(); |
| for (ICompilationUnit cu : memberMap.keySet()) { |
| members.addAll(getMembers(cu)); |
| } |
| return members.toArray(new IMember[members.size()]); |
| } |
| |
| private Collection<IMember> getMembers(ICompilationUnit cu) { |
| Collection<IMember> result = memberMap.get(cu); |
| if (result==null) { |
| result = new ArrayList<IMember>(); |
| memberMap.put(cu, result); |
| } |
| return result; |
| } |
| |
| @Override |
| public String getName() { |
| return "Pull-Out"; |
| } |
| |
| public boolean hasMembers() { |
| return !memberSet.isEmpty(); |
| } |
| |
| public RefactoringStatus initialize(Map<String, String> args) { |
| RefactoringStatus status = new RefactoringStatus(); |
| setMakePrivileged(Boolean.valueOf(args.get(MAKE_PRIVILEGED))); |
| setMember((IMember)JavaCore.create(args.get(MEMBER)), status); |
| return status; |
| } |
| |
| private boolean isInTopLevelType(IMember member) { |
| IJavaElement parent = member.getParent(); |
| Assert.isTrue(parent.getElementType()==IJavaElement.TYPE); |
| return parent.getParent().getElementType()==IJavaElement.COMPILATION_UNIT; |
| } |
| |
| /** |
| * Is the "make privileged" option of the refactoring set. |
| */ |
| public boolean isMakePrivileged() { |
| return makePrivileged; |
| } |
| |
| /** |
| * Does given IBinding refer to an IJavaElement that will be moved into the Aspect? |
| */ |
| private boolean isMoved(IBinding binding) { |
| return isPulled(binding.getJavaElement()); |
| } |
| |
| /** |
| * Will the given IJavaElement be moved into the Aspect? This test returns true, also for |
| * IJavaElements that are contained inside pulled elements! |
| */ |
| public boolean isMoved(IJavaElement javaElement) { |
| //Null case is handled to easily terminate recursion |
| return javaElement!=null |
| && ( isPulled(javaElement) || isMoved(javaElement.getParent()) ); |
| } |
| |
| /** |
| * Is the target aspect privileged before the refactoring? |
| */ |
| private boolean isPrivileged() { |
| if (targetAspect==null) |
| return false; |
| try { |
| return targetAspect.isPrivileged(); |
| } catch (JavaModelException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * Are we allowed to delete the abstract modifier and fabricate |
| * dummy method bodies? |
| */ |
| public boolean isGenerateAbstractMethodStubs() { |
| return generateAbstractMethodStubs; |
| } |
| |
| /** |
| * Allow pulling out of abstract methods. The abstract keyword will |
| * be removed and a dummy method body added. |
| */ |
| public void setGenerateAbstractMethodStubs(boolean allow) { |
| this.generateAbstractMethodStubs = allow; |
| } |
| |
| /** |
| * Allow ITDs to be made public, as needed. |
| */ |
| public void setAllowMakePublic(boolean allow) { |
| this.allowMakePublic = allow; |
| } |
| |
| /** |
| * Allow ITDs to be made public, as needed. |
| */ |
| public void setAllowDeleteProtected(boolean allow) { |
| this.allowDeleteProtected = allow; |
| } |
| |
| /** |
| * Are we allowed to delete any visibility modifier (on ITDs) |
| * and make the ITD public? |
| */ |
| public boolean isAllowMakePublic() { |
| return allowMakePublic; |
| } |
| |
| /** |
| * Are we allowed to delete the protected keyword from ITDs? |
| */ |
| public boolean isAllowDeleteProtected() { |
| return allowDeleteProtected || allowMakePublic; |
| } |
| |
| /** |
| * Is the given IJaveElement selected to be pulled into the Aspect. Elements moved because they are |
| * nested inside selected elements are *not* considered (if you want this, use isMoved instead). |
| */ |
| private boolean isPulled(IJavaElement javaElement) { |
| return memberSet.contains(javaElement); |
| } |
| |
| private static RefactoringStatusContext makeContext(ICompilationUnit cu, ASTNode node) { |
| return JavaStatusContext.create(cu, |
| new SourceRange(node.getStartPosition(), node.getLength())); |
| } |
| |
| private static RefactoringStatusContext makeContext(IMember member) { |
| try { |
| return JavaStatusContext.create(member.getCompilationUnit(), member.getSourceRange()); |
| } catch (JavaModelException e) { |
| return null; // Too bad, no context for the error message |
| } |
| } |
| |
| static RefactoringStatusContext makeContext(IMember member, ASTNode node) { |
| return makeContext(member.getCompilationUnit(), node); |
| } |
| |
| private static RefactoringStatusContext makeContext(SearchMatch match) { |
| try { |
| IJavaElement element = (IJavaElement) match.getElement(); |
| ITypeRoot typeRoot = (ITypeRoot) element.getAncestor(IJavaElement.COMPILATION_UNIT); |
| if (typeRoot==null) { |
| typeRoot = (ITypeRoot) element.getAncestor(IJavaElement.CLASS_FILE); |
| } |
| ISourceRange range = new SourceRange(match.getOffset(), match.getLength()); |
| return JavaStatusContext.create(typeRoot, range); |
| } |
| catch (Throwable e) { |
| return null; |
| } |
| } |
| |
| |
| public void setAspect(AspectElement target) { |
| this.targetAspect = target; |
| } |
| |
| /** |
| * Set the target aspect by giving the name of the aspect. Note that this method |
| * only works if it can figure out what project to look for the aspect, so at least |
| * one member to be pulled out has to be set prior to calling this method. |
| */ |
| public RefactoringStatus setAspect(String name) { |
| IType type= null; |
| |
| try { |
| if (name.length() == 0) |
| return RefactoringStatus.createFatalErrorStatus("Select an Aspect."); |
| |
| type= getJavaProject().findType(name, new NullProgressMonitor()); |
| if (type == null || !type.exists()) |
| return RefactoringStatus.createErrorStatus(MessageFormat.format("Aspect ''{0}'' does not exist.", name)); |
| if (!(type instanceof AspectElement)) |
| return RefactoringStatus.createErrorStatus(MessageFormat.format("''{0}'' is not an Aspect.", name)); |
| } catch (JavaModelException exception) { |
| return RefactoringStatus.createFatalErrorStatus("Could not determine type."); |
| } |
| |
| if (type.isReadOnly()) |
| return RefactoringStatus.createFatalErrorStatus("Type is read-only."); |
| |
| if (type.isBinary()) |
| return RefactoringStatus.createFatalErrorStatus("Type is binary."); |
| |
| targetAspect = (AspectElement) type; |
| |
| return new RefactoringStatus(); |
| } |
| |
| /** |
| * Set the "make privileged" option of the refactoring. |
| */ |
| public void setMakePrivileged(boolean makePrivileged) { |
| this.makePrivileged = makePrivileged; |
| } |
| |
| public void setMember(IMember member, RefactoringStatus status) { |
| clearMembers(); |
| addMember(member, status); |
| } |
| |
| /** |
| * Will the target aspect be privileged after refactoring |
| */ |
| public boolean willBePrivileged() { |
| return isPrivileged() || isMakePrivileged(); |
| } |
| |
| protected String getAbstractMethodStubBody(IMember originalMember) { |
| //FIXKDV: Stupid implementation for now... maybe we can use the Eclipse Java Code templates somehow |
| // or have the user specify their own abstract method stub template in the wizard. |
| return " { throw new Error(\"abstract method stub\"); }"; |
| } |
| } |