Java tutorial
/******************************************************************************* * 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.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.codemanipulation.ImportReferencesCollector; 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\"); }"; } }