blob: 0156bc7fce289267a5474a7d37cb2d32b73e458e [file] [log] [blame]
/*******************************************************************************
* 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\"); }";
}
}