blob: 3216e08e59147cc804883ec666ec2c26581a28fe [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.refactoring.java.plugins;
import com.sun.source.tree.*;
import com.sun.source.util.TreePath;
import java.io.IOException;
import java.util.*;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.*;
import org.netbeans.modules.refactoring.api.AbstractRefactoring;
import org.netbeans.modules.refactoring.api.Problem;
import org.netbeans.modules.refactoring.java.RefactoringUtils;
import org.netbeans.modules.refactoring.java.api.ExtractInterfaceRefactoring;
import org.netbeans.modules.refactoring.java.api.JavaRefactoringUtils;
import org.netbeans.modules.refactoring.java.spi.DiffElement;
import org.netbeans.modules.refactoring.java.spi.JavaRefactoringPlugin;
import org.netbeans.modules.refactoring.spi.RefactoringElementsBag;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
/**
* Plugin that implements the core functionality of Extract Interface refactoring.
* <br>Extracts: <ul>
* <li>implements interfaces</li>
* <li>public nonstatic methods</li>
* <li>public static final fields</li>
* <li>XXX public static class/interface/enum/annotation type.<br><i>dangerous, it might contain
* elements that will be unaccessible from the new interface. Maybe reusing Move Class refactoring
* would be appropriate. Not implemented in 6.0 yet. Pre-6.0 implementation was not solved references at all.</i></li>
* </ul>
* XXX there should be option Copy/Move/AsIs javadoc.
*
* @author Martin Matula, Jan Pokorsky
*/
public final class ExtractInterfaceRefactoringPlugin extends JavaRefactoringPlugin {
/** Reference to the parent refactoring instance */
private final ExtractInterfaceRefactoring refactoring;
private String pkgName;
/** class for extracting interface */
private ElementHandle<TypeElement> classHandle;
/** Creates a new instance of ExtractInterfaceRefactoringPlugin
* @param refactoring Parent refactoring instance.
*/
ExtractInterfaceRefactoringPlugin(ExtractInterfaceRefactoring refactoring) {
this.refactoring = refactoring;
}
@Override
public Problem fastCheckParameters() {
Problem result = null;
String newName = refactoring.getInterfaceName();
if (!Utilities.isJavaIdentifier(newName)) {
result = createProblem(result, true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_InvalidIdentifier", newName)); // NOI18N
return result;
}
FileObject primFile = refactoring.getSourceType().getFileObject();
FileObject folder = primFile.getParent();
FileObject[] children = folder.getChildren();
for (FileObject child: children) {
if (!child.isVirtual() && child.getName().equalsIgnoreCase(newName) && "java".equalsIgnoreCase(child.getExt())) { // NOI18N
result = createProblem(result, true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ClassClash", newName, pkgName)); // NOI18N
return result;
}
}
return super.fastCheckParameters();
}
@Override
protected Problem fastCheckParameters(CompilationController javac) throws IOException {
Problem result = null;
String newName = refactoring.getInterfaceName();
TypeMirror parsedType = javac.getTreeUtilities().parseType(newName, classHandle.resolve(javac));
if(parsedType != null && parsedType.getKind() != TypeKind.ERROR) {
result = createProblem(result, true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ClassClash", newName, pkgName)); // NOI18N
return result;
}
return super.fastCheckParameters(javac);
}
@Override
public Problem prepare(RefactoringElementsBag bag) {
FileObject primFile = refactoring.getSourceType().getFileObject();
try {
UpdateClassTask.create(bag, primFile, refactoring, classHandle);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return null;
}
@Override
protected JavaSource getJavaSource(Phase p) {
return JavaSource.forFileObject(refactoring.getSourceType().getFileObject());
}
@Override
protected Problem preCheck(CompilationController javac) throws IOException {
// fire operation start on the registered progress listeners (1 step)
fireProgressListenerStart(AbstractRefactoring.PRE_CHECK, 1);
javac.toPhase(JavaSource.Phase.RESOLVED);
try {
TreePathHandle sourceType = refactoring.getSourceType();
// check whether the element is valid
Problem result = isElementAvail(sourceType, javac);
if (result != null) {
// fatal error -> don't continue with further checks
return result;
}
// check whether the element is an unresolved class
Element sourceElm = sourceType.resolveElement(javac);
result = JavaPluginUtils.isSourceElement(sourceElm, javac);
if (result != null) {
return result;
}
if (sourceElm == null || (sourceElm.getKind() != ElementKind.CLASS && sourceElm.getKind() != ElementKind.INTERFACE && sourceElm.getKind() != ElementKind.ENUM)) {
// fatal error -> return
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ElementNotAvailable")); // NOI18N
}
classHandle = ElementHandle.<TypeElement>create((TypeElement) sourceElm);
PackageElement pkgElm = (PackageElement) javac.getElementUtilities().outermostTypeElement(sourceElm).getEnclosingElement();
pkgName = pkgElm.getQualifiedName().toString();
// increase progress (step 1)
fireProgressListenerStep();
// all checks passed -> return null
return null;
} finally {
// fire operation end on the registered progress listeners
fireProgressListenerStop();
}
}
@Override
protected Problem checkParameters(CompilationController javac) throws IOException {
if (refactoring.getMethods().isEmpty() && refactoring.getFields().isEmpty() && refactoring.getImplements().isEmpty()) {
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ExtractInterface_MembersNotAvailable")); // NOI18N);
}
// check whether the selected members are public and non-static in case of methods, static in other cases
// check whether all members belong to the source type
// XXX check if method params and return type will be accessible after extraction; likely not fatal
javac.toPhase(JavaSource.Phase.RESOLVED);
TypeElement sourceType = (TypeElement) refactoring.getSourceType().resolveElement(javac);
assert sourceType != null;
Set<? extends Element> members = new HashSet<Element>(sourceType.getEnclosedElements());
for (ElementHandle<ExecutableElement> elementHandle : refactoring.getMethods()) {
ExecutableElement elm = elementHandle.resolve(javac);
if (elm == null) {
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ElementNotAvailable")); // NOI18N
}
if (javac.getElementUtilities().isSynthetic(elm) || elm.getKind() != ElementKind.METHOD) {
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ExtractInterface_UnknownMember", // NOI18N
elm.toString()));
}
if (!members.contains(elm)) {
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ExtractInterface_UnknownMember", // NOI18N
elm.toString()));
}
Set<Modifier> mods = elm.getModifiers();
if (!mods.contains(Modifier.PUBLIC) || mods.contains(Modifier.STATIC)) {
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ExtractInterface_WrongModifiers", elm.getSimpleName().toString())); // NOI18N
}
}
for (ElementHandle<VariableElement> elementHandle : refactoring.getFields()) {
VariableElement elm = elementHandle.resolve(javac);
if (elm == null) {
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ElementNotAvailable")); // NOI18N
}
if (javac.getElementUtilities().isSynthetic(elm) || elm.getKind() != ElementKind.FIELD) {
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ExtractInterface_UnknownMember", // NOI18N
elm.toString()));
}
if (!members.contains(elm)) {
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ExtractInterface_UnknownMember", // NOI18N
elm.toString()));
}
Set<Modifier> mods = elm.getModifiers();
if (mods.contains(Modifier.PUBLIC) && mods.contains(Modifier.STATIC) && mods.contains(Modifier.FINAL)) {
VariableTree tree = (VariableTree) javac.getTrees().getTree(elm);
if (tree.getInitializer() != null) {
continue;
}
}
return new Problem(true, NbBundle.getMessage(ExtractInterfaceRefactoringPlugin.class, "ERR_ExtractInterface_WrongModifiers", elm.getSimpleName().toString())); // NOI18N
}
// XXX check refactoring.getImplements()
return null;
}
/**
* Finds all type parameters of <code>javaClass</code> that are referenced by
* any member that is going to be extract.
* @param refactoring the refactoring containing members to extract
* @param javac compilation info
* @param javaClass java class declaring parameters to find
* @return type parameters to extract
*/
private static List<TypeMirror> findUsedGenericTypes(ExtractInterfaceRefactoring refactoring, CompilationInfo javac, TypeElement javaClass) {
List<TypeMirror> typeArgs = JavaRefactoringUtils.elementsToTypes(javaClass.getTypeParameters());
if (typeArgs.isEmpty()) {
return typeArgs;
}
Types typeUtils = javac.getTypes();
Set<TypeMirror> used = Collections.newSetFromMap(new IdentityHashMap<TypeMirror, Boolean>());
// do not check fields since static fields cannot use type parameter of the enclosing class
// check methods
for (Iterator<ElementHandle<ExecutableElement>> methodIter = refactoring.getMethods().iterator(); methodIter.hasNext() && !typeArgs.isEmpty();) {
ElementHandle<ExecutableElement> handle = methodIter.next();
ExecutableElement elm = handle.resolve(javac);
RefactoringUtils.findUsedGenericTypes(typeUtils, typeArgs, used, elm.getReturnType());
for (Iterator<? extends VariableElement> paramIter = elm.getParameters().iterator(); paramIter.hasNext() && !typeArgs.isEmpty();) {
VariableElement param = paramIter.next();
RefactoringUtils.findUsedGenericTypes(typeUtils, typeArgs, used, param.asType());
}
}
// check implements
for (Iterator<TypeMirrorHandle<TypeMirror>> it = refactoring.getImplements().iterator(); it.hasNext() && !typeArgs.isEmpty();) {
TypeMirrorHandle<TypeMirror> handle = it.next();
TypeMirror implemetz = handle.resolve(javac);
RefactoringUtils.findUsedGenericTypes(typeUtils, typeArgs, used, implemetz);
}
return RefactoringUtils.filterTypes(typeArgs, used);
}
// --- REFACTORING ELEMENTS ------------------------------------------------
private final static class UpdateClassTask implements CancellableTask<WorkingCopy> {
private final ExtractInterfaceRefactoring refactoring;
private final ElementHandle<TypeElement> sourceType;
private UpdateClassTask(ExtractInterfaceRefactoring refactoring, ElementHandle<TypeElement> sourceType) {
this.sourceType = sourceType;
this.refactoring = refactoring;
}
public static void create(RefactoringElementsBag bag, FileObject fo, ExtractInterfaceRefactoring refactoring, ElementHandle<TypeElement> sourceType) throws IOException {
JavaSource js = JavaSource.forFileObject(fo);
ModificationResult modification = js.runModificationTask(new UpdateClassTask(refactoring, sourceType));
List<? extends ModificationResult.Difference> diffs = modification.getDifferences(fo);
for (ModificationResult.Difference diff : diffs) {
bag.add(refactoring, DiffElement.create(diff, fo, modification));
}
bag.registerTransaction(createTransaction(Collections.singletonList(modification)));
}
@Override
public void cancel() {
}
@Override
public void run(WorkingCopy wc) throws Exception {
wc.toPhase(JavaSource.Phase.RESOLVED);
createCu(wc);
TypeElement clazz = this.sourceType.resolve(wc);
assert clazz != null;
ClassTree classTree = wc.getTrees().getTree(clazz);
TreeMaker maker = wc.getTreeMaker();
// fake interface since interface file does not exist yet
Tree interfaceTree;
List<TypeMirror> typeParams = findUsedGenericTypes(refactoring, wc, clazz);
if (typeParams.isEmpty()) {
interfaceTree = maker.Identifier(refactoring.getInterfaceName());
} else {
List<ExpressionTree> typeParamTrees = new ArrayList<ExpressionTree>(typeParams.size());
for (TypeMirror typeParam : typeParams) {
Tree t = maker.Type(typeParam);
typeParamTrees.add((ExpressionTree) t);
}
interfaceTree = maker.ParameterizedType(
maker.Identifier(refactoring.getInterfaceName()),
typeParamTrees
);
}
Set<Tree> members2Remove = new HashSet<Tree>();
Set<Tree> interfaces2Remove = new HashSet<Tree>();
members2Remove.addAll(getFields2Remove(wc, refactoring.getFields()));
members2Remove.addAll(getMethods2Remove(wc, refactoring.getMethods(), clazz));
interfaces2Remove.addAll(getImplements2Remove(wc, refactoring.getImplements(), clazz));
for (ElementHandle el : refactoring.getMethods()) {
ExecutableElement e = (ExecutableElement) el.resolve(wc);
Tree tree = wc.getTrees().getTree(e);
MethodTree mt = (MethodTree) tree;
if (e.getAnnotation(Override.class)==null) {
TreeMaker make = wc.getTreeMaker();
AnnotationTree ann = make.Annotation(make.Identifier("Override"), Collections.<ExpressionTree>emptyList());
ModifiersTree modifiers = wc.getTreeMaker().addModifiersAnnotation(mt.getModifiers(), ann);
wc.rewrite(mt.getModifiers(), modifiers);
}
}
// filter out obsolete members
List<Tree> members2Add = new ArrayList<Tree>();
for (Tree tree : classTree.getMembers()) {
if (!members2Remove.contains(tree)) {
members2Add.add(tree);
}
}
// filter out obsolete implements trees
List<Tree> impls2Add = resolveImplements(classTree.getImplementsClause(), interfaces2Remove, interfaceTree);
ClassTree nc;
if (clazz.getKind() == ElementKind.CLASS) {
nc = maker.Class(
classTree.getModifiers(),
classTree.getSimpleName(),
classTree.getTypeParameters(),
classTree.getExtendsClause(),
impls2Add,
members2Add);
} else if (clazz.getKind() == ElementKind.INTERFACE) {
nc = maker.Interface(
classTree.getModifiers(),
classTree.getSimpleName(),
classTree.getTypeParameters(),
impls2Add,
members2Add);
} else if (clazz.getKind() == ElementKind.ENUM) {
nc = maker.Enum(
classTree.getModifiers(),
classTree.getSimpleName(),
impls2Add,
members2Add);
} else {
throw new IllegalStateException(classTree.toString());
}
wc.rewrite(classTree, nc);
}
private List<Tree> getFields2Remove(CompilationInfo javac, List<ElementHandle<VariableElement>> members) {
if (members.isEmpty()) {
return Collections.<Tree>emptyList();
}
List<Tree> result = new ArrayList<Tree>(members.size());
for (ElementHandle<VariableElement> handle : members) {
VariableElement elm = handle.resolve(javac);
assert elm != null;
Tree t = javac.getTrees().getTree(elm);
assert t != null;
result.add(t);
}
return result;
}
private List<Tree> getMethods2Remove(CompilationInfo javac, List<ElementHandle<ExecutableElement>> members, TypeElement clazz) {
if (members.isEmpty()) {
return Collections.<Tree>emptyList();
}
boolean isInterface = clazz.getKind() == ElementKind.INTERFACE;
List<Tree> result = new ArrayList<Tree>(members.size());
for (ElementHandle<ExecutableElement> handle : members) {
ExecutableElement elm = handle.resolve(javac);
assert elm != null;
if (isInterface || elm.getModifiers().contains(Modifier.ABSTRACT)) {
// it is interface method nor abstract method
Tree t = javac.getTrees().getTree(elm);
assert t != null;
result.add(t);
}
}
return result;
}
private List<Tree> getImplements2Remove(CompilationInfo javac, List<TypeMirrorHandle<TypeMirror>> members, TypeElement clazz) {
if (members.isEmpty()) {
return Collections.<Tree>emptyList();
}
// resolve members to remove
List<TypeMirror> memberTypes = new ArrayList<TypeMirror>(members.size());
for (TypeMirrorHandle<TypeMirror> handle : members) {
TypeMirror tm = handle.resolve(javac);
memberTypes.add(tm);
}
ClassTree classTree = javac.getTrees().getTree(clazz);
List<Tree> result = new ArrayList<Tree>();
Types types = javac.getTypes();
// map TypeMirror to Tree
for (Tree tree : classTree.getImplementsClause()) {
TreePath path = javac.getTrees().getPath(javac.getCompilationUnit(), tree);
TypeMirror existingTM = javac.getTrees().getTypeMirror(path);
for (TypeMirror tm : memberTypes) {
if (types.isSameType(tm, existingTM)) {
result.add(tree);
break;
}
}
}
return result;
}
private static List<Tree> resolveImplements(List<? extends Tree> allImpls, Set<Tree> impls2Remove, Tree impl2Add) {
List<Tree> ret;
if (allImpls == null) {
ret = new ArrayList<Tree>(1);
} else {
ret = new ArrayList<Tree>(allImpls.size() + 1);
ret.addAll(allImpls);
}
if (impls2Remove != null && !impls2Remove.isEmpty()) {
ret.removeAll(impls2Remove);
}
ret.add(impl2Add);
return ret;
}
public void createCu(WorkingCopy wc) throws Exception {
wc.toPhase(JavaSource.Phase.RESOLVED);
TreeMaker make = wc.getTreeMaker();
GeneratorUtilities genUtils = GeneratorUtilities.get(wc);
// add type parameters
List<TypeMirror> typeParams = findUsedGenericTypes(refactoring, wc, sourceType.resolve(wc));
List<TypeParameterTree> newTypeParams = new ArrayList<TypeParameterTree>(typeParams.size());
// lets retrieve param type trees from origin class since it is
// almost impossible to create them via TreeMaker
TypeElement sourceTypeElm = sourceType.resolve(wc);
for (TypeParameterElement typeParam : sourceTypeElm.getTypeParameters()) {
TypeMirror origParam = typeParam.asType();
for (TypeMirror newParam : typeParams) {
if (wc.getTypes().isSameType(origParam, newParam)) {
Tree t = wc.getTrees().getTree(typeParam);
if (t.getKind() == Tree.Kind.TYPE_PARAMETER) {
TypeParameterTree typeParamTree = (TypeParameterTree) t;
if (!typeParamTree.getBounds().isEmpty()) {
typeParamTree = (TypeParameterTree) genUtils.importFQNs(t);
}
newTypeParams.add(typeParamTree);
}
}
}
}
// add new fields
List<Tree> members = new ArrayList<Tree>();
for (ElementHandle<VariableElement> handle : refactoring.getFields()) {
VariableElement memberElm = handle.resolve(wc);
VariableTree tree = (VariableTree) wc.getTrees().getTree(memberElm);
VariableTree newVarTree = make.Variable(
make.Modifiers(Collections.<Modifier>emptySet(), tree.getModifiers().getAnnotations()),
tree.getName(),
tree.getType(),
tree.getInitializer());
newVarTree = genUtils.importFQNs(newVarTree);
tree = genUtils.importComments(tree, wc.getTrees().getPath(memberElm).getCompilationUnit());
genUtils.copyComments(tree, newVarTree, false);
genUtils.copyComments(tree, newVarTree, true);
members.add(newVarTree);
}
// add newmethods
for (ElementHandle<ExecutableElement> handle : refactoring.getMethods()) {
ExecutableElement memberElm = handle.resolve(wc);
TreePath mpath = wc.getTrees().getPath(memberElm);
MethodTree tree = wc.getTrees().getTree(memberElm);
List<? extends AnnotationTree> annotations =
filterOutOverrideAnnotation(tree.getModifiers().getAnnotations(), wc, mpath);
MethodTree newMethodTree = make.Method(
make.Modifiers(Collections.<Modifier>emptySet(), annotations),
tree.getName(),
tree.getReturnType(),
tree.getTypeParameters(),
tree.getParameters(),
tree.getThrows(),
(BlockTree) null,
null);
newMethodTree = genUtils.importFQNs(newMethodTree);
tree = genUtils.importComments(tree, wc.getTrees().getPath(memberElm).getCompilationUnit());
genUtils.copyComments(tree, newMethodTree, false);
genUtils.copyComments(tree, newMethodTree, true);
members.add(newMethodTree);
}
// add super interfaces
List <Tree> extendsList = new ArrayList<Tree>();
for (TypeMirrorHandle<? extends TypeMirror> handle : refactoring.getImplements()) {
// XXX check if interface is not aready there; the templates might be changed by user :-(
TypeMirror implMirror = handle.resolve(wc);
extendsList.add(make.Type(implMirror));
}
// create new interface
ClassTree newInterfaceTree = make.Interface(
make.Modifiers(EnumSet.of(Modifier.PUBLIC)),
refactoring.getInterfaceName(),
newTypeParams,
extendsList,
Collections.<Tree>emptyList());
newInterfaceTree = genUtils.insertClassMembers(newInterfaceTree, members);
FileObject fileObject = refactoring.getSourceType().getFileObject();
FileObject sourceRoot = ClassPath.getClassPath(fileObject, ClassPath.SOURCE).findOwnerRoot(fileObject);
String relativePath = FileUtil.getRelativePath(sourceRoot, fileObject.getParent()) + "/" + refactoring.getInterfaceName() + ".java";
CompilationUnitTree cu = JavaPluginUtils.createCompilationUnit(sourceRoot, relativePath, newInterfaceTree, wc, make);
wc.rewrite(null, cu);
}
// --- helper methods ----------------------------------
private static List<? extends AnnotationTree> filterOutOverrideAnnotation(List<? extends AnnotationTree> annotations, CompilationInfo javac, TreePath pathToMethod) {
if (annotations.isEmpty()) {
return annotations;
}
List<AnnotationTree> newAnnotations = new ArrayList<AnnotationTree>(annotations.size());
TypeElement overrideAnn = javac.getElements().getTypeElement("java.lang.Override"); // NOI18N
for (AnnotationTree annotationTree : annotations) {
Element annotation = javac.getTrees().getElement(new TreePath(pathToMethod, annotationTree));
if (annotation == null || annotation != overrideAnn) {
newAnnotations.add(annotationTree);
}
}
return newAnnotations;
}
}
}