/**
* Copyright (C) 2005 - 2013 Eric Van Dewoestine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.eclim.plugin.jdt.command.impl;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.eclim.Services;
import org.eclim.annotation.Command;
import org.eclim.command.CommandLine;
import org.eclim.command.Options;
import org.eclim.plugin.core.command.AbstractCommand;
import org.eclim.plugin.jdt.util.JavaUtils;
import org.eclim.plugin.jdt.util.MethodUtils;
import org.eclim.plugin.jdt.util.TypeUtils;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IParent;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IPackageBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jdt.internal.corext.codemanipulation.AddUnimplementedMethodsOperation;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility2;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser;
import org.eclipse.jdt.internal.ui.javaeditor.ASTProvider;
import com.google.gson.Gson;
/**
* Command used to build a tree of methods that have or can be
* implemented/overriden by the supplied file according the interfaces/parent
* class it implements/extends.
*
* @author Eric Van Dewoestine
*/
@Command(
name = "java_impl",
options =
"REQUIRED p project ARG," +
"REQUIRED f file ARG," +
"OPTIONAL o offset ARG," +
"OPTIONAL e encoding ARG," +
"OPTIONAL t type ARG," +
"OPTIONAL s superType ARG," +
"OPTIONAL m methods ARG"
)
public class ImplCommand
extends AbstractCommand
{
@Override
public Object execute(CommandLine commandLine)
throws Exception
{
String project = commandLine.getValue(Options.PROJECT_OPTION);
String file = commandLine.getValue(Options.FILE_OPTION);
ICompilationUnit src = JavaUtils.getCompilationUnit(project, file);
IType type = getType(src, commandLine);
// if a supertype option is supplied, then add methods as necessary.
if (commandLine.hasOption(Options.SUPERTYPE_OPTION)){
insertMethods(src, type, commandLine);
}
return getImplResult(src, type);
}
/**
* Gets the type to be edited.
*
* @param src The ICompilationUnit of the source file to edit.
* @param commandLine The command line.
* @return The IType to be edited.
*/
protected IType getType(ICompilationUnit src, CommandLine commandLine)
throws Exception
{
IType type = null;
// If a qualified name for the type being modified was supplied
if (commandLine.hasOption(Options.TYPE_OPTION)) {
IJavaProject javaProject = src.getJavaProject();
String typeFQN = commandLine.getValue(Options.TYPE_OPTION);
int indexOfDollar = typeFQN.indexOf("$");
// If we are dealing with an anonymous inner class, findType does not work
// so lets find it starting at the class containing the anonymous class.
if (indexOfDollar > 0) {
String primaryTypeFQN = typeFQN.substring(0, indexOfDollar);
IType primaryType = javaProject.findType(primaryTypeFQN);
LinkedList<IJavaElement> todo = new LinkedList<IJavaElement>();
todo.add(primaryType);
while (!todo.isEmpty()) {
IJavaElement element = todo.removeFirst();
if (element instanceof IType) {
IType tempType = (IType) element;
String name = tempType.getFullyQualifiedName();
if (name.equals(typeFQN)) {
type = tempType;
break;
}
}
if (element instanceof IParent) {
for (IJavaElement child : ((IParent)element).getChildren()) {
todo.add(child);
}
}
}
// Else it is a normal class/interface so find works
} else {
type = javaProject.findType(typeFQN);
}
// If not, we need to find it based on the current selection
} else {
type = TypeUtils.getType(src, getOffset(commandLine));
}
return type;
}
/**
* Get the operation used to add the requested methods.
*
* @param src The ICompilationUnit of the source file to edit.
* @param type The IType of the type in the source to edit.
* @param chosen A set containing method signatures to add or null to add all
* methods from the chosen super type.
* @param sibling The sibling to insert the methods before.
* @param pos The position of the sibling.
* @param commandLine The command line.
*
* @return An IWorkspaceRunnable
*/
protected IWorkspaceRunnable getImplOperation(
ICompilationUnit src,
IType type,
Set<String> chosen,
IJavaElement sibling,
int pos,
CommandLine commandLine)
throws Exception
{
RefactoringASTParser parser =
new RefactoringASTParser(ASTProvider.SHARED_AST_LEVEL);
CompilationUnit cu = parser.parse(type.getCompilationUnit(), true);
ITypeBinding typeBinding = ASTNodes.getTypeBinding(cu, type);
String superType = commandLine.getValue(Options.SUPERTYPE_OPTION);
List<IMethodBinding> overridable = getOverridableMethods(cu, typeBinding);
List<IMethodBinding> override = new ArrayList<IMethodBinding>();
for (IMethodBinding binding : overridable){
ITypeBinding declBinding = binding.getDeclaringClass();
String fqn = declBinding.getQualifiedName().replaceAll("<.*?>", "");
if (fqn.equals(superType) && isChosen(chosen, binding)){
override.add(binding);
}
}
if (override.size() > 0){
return new AddUnimplementedMethodsOperation(
cu, typeBinding,
override.toArray(new IMethodBinding[override.size()]),
pos, true, true, true);
}
return null;
}
/**
* Checks if the supplied IMethodBinding is in the set of chosen methods to
* insert.
*
* @param chosen The set of chosen method signatures.
* @param methodBinding The IMethodBinding to check.
* @return True if the method is in the chosen set, false otherwise.
*/
protected boolean isChosen(Set<String> chosen, IMethodBinding methodBinding)
{
return chosen == null ||
chosen.contains(getMethodBindingShortCallSignature(methodBinding));
}
/**
* Get the ImplResult containing super types and their methods that can be
* added to the supplied source.
*
* @param src The ICompilationUnit of the source file.
* @param type The IType of the type in the source that methods would be
* modified.
* @return An ImplResult
*/
protected ImplResult getImplResult(ICompilationUnit src, IType type)
throws Exception
{
List<IMethodBinding> overridable = getOverridableMethods(src, type);
return getImplResult(type.getFullyQualifiedName(), overridable);
}
/**
* Get the ImplResult containing super types and their methods that can be
* added to the supplied source.
*
* @param name The name of the type in the source that methods would be
* added to.
* @param methods List of IMethodBinding representing the available methods.
* @return An ImplResult
*/
protected ImplResult getImplResult(String name, List<IMethodBinding> methods)
{
ArrayList<ImplType> results = new ArrayList<ImplType>();
ArrayList<String> overrideMethods = null;
ITypeBinding curTypeBinding = null;
for (IMethodBinding methodBinding : methods) {
ITypeBinding typeBinding = methodBinding.getDeclaringClass();
if (typeBinding != curTypeBinding){
if (overrideMethods != null && overrideMethods.size() > 0){
results.add(createImplType(curTypeBinding, overrideMethods));
}
overrideMethods = new ArrayList<String>();
}
curTypeBinding = typeBinding;
overrideMethods.add(getMethodBindingSignature(methodBinding));
}
if (overrideMethods != null && overrideMethods.size() > 0){
results.add(createImplType(curTypeBinding, overrideMethods));
}
return new ImplResult(name, results);
}
/**
* Gets a list of overridable IMethodBindings.
*
* @param src The source file.
* @param type The type within the source file.
* @return List of IMethodBinding.
*/
protected List<IMethodBinding> getOverridableMethods(
ICompilationUnit src, IType type)
throws Exception
{
RefactoringASTParser parser =
new RefactoringASTParser(ASTProvider.SHARED_AST_LEVEL);
CompilationUnit cu = parser.parse(type.getCompilationUnit(), true);
ITypeBinding typeBinding = ASTNodes.getTypeBinding(cu, type);
return getOverridableMethods(cu, typeBinding);
}
/**
* Gets a list of overridable IMethodBindings.
*
* @param cu AST CompilationUnit.
* @param typeBinding The binding of the type with the CompilationUnit.
* @return List of IMethodBinding.
*/
protected List<IMethodBinding> getOverridableMethods(
CompilationUnit cu, ITypeBinding typeBinding)
throws Exception
{
if(!typeBinding.isClass()){
throw new IllegalArgumentException(
Services.getMessage("type.not.a.class", typeBinding.getQualifiedName()));
}
IPackageBinding packageBinding = typeBinding.getPackage();
IMethodBinding[] methods =
StubUtility2.getOverridableMethods(cu.getAST(), typeBinding, false);
ArrayList<IMethodBinding> overridable = new ArrayList<IMethodBinding>();
for (IMethodBinding methodBinding : methods) {
if (Bindings.isVisibleInHierarchy(methodBinding, packageBinding)){
overridable.add(methodBinding);
}
}
return overridable;
}
private ImplType createImplType(
ITypeBinding typeBinding, List<String> overridable)
{
String packageName = typeBinding.getPackage().getName();
String qualifiedBindingName = typeBinding.getQualifiedName();
// Take the remaining part after package name (and following dot) to be the
// binding name so as to correctly identify nested classes/interfaces
String bindingName = qualifiedBindingName.substring(packageName.length() + 1);
String signature =
(typeBinding.isInterface() ? "interface " : "class ") +
bindingName.replaceAll("(\\bjava\\.lang\\.|#RAW)", "");
return new ImplType(
packageName,
signature,
overridable.toArray(new String[overridable.size()]));
}
private void insertMethods(
ICompilationUnit src, IType type, CommandLine commandLine)
throws Exception
{
String methodsOption = commandLine.getValue(Options.METHOD_OPTION);
HashSet<String> chosen = null;
if(methodsOption != null){
chosen = new HashSet<String>();
String[] sigs = new Gson().fromJson(methodsOption, String[].class);
for (String sig : sigs){
chosen.add(sig.replace(" ", ""));
}
}
int pos = -1;
int len = src.getBuffer().getLength();
final IJavaElement sibling;
if (commandLine.hasOption(Options.OFFSET_OPTION)) {
sibling = getSibling(type, getOffset(commandLine));
} else {
sibling = getSibling(type);
}
if (sibling != null){
pos = getOffset(sibling);
}
IWorkspaceRunnable op = getImplOperation(
src, type, chosen, sibling, pos, commandLine);
if (op != null){
String lineDelim = src.findRecommendedLineSeparator();
IImportDeclaration[] imports = src.getImports();
int importsEnd = -1;
if (imports.length > 0){
ISourceRange last = imports[imports.length - 1].getSourceRange();
importsEnd = last.getOffset() + last.getLength() + lineDelim.length();
}
op.run(null);
// an op.getResultingEdit() would be nice here, but we'll make do w/ what
// we got and caculate our own edit offset/length combo so we can format
// the new code.
int offset = pos != -1 ? pos : (len - 1 - lineDelim.length());
int newLen = src.getBuffer().getLength();
int length = newLen - len - 1;
// a more accurate length estimate can be found by locating the
// sibling at its new position and taking the difference.
// this prevents the formatting from screwing up the sibling's formatting
final IJavaElement[] newSiblings = sibling == null ?
null : src.findElements(sibling);
if (newSiblings != null && newSiblings.length == 1) {
// not sure what it would mean if there were more than one...
length = getOffset(newSiblings[0]) - offset;
}
// the change in length may include newly added imports, so handle that as
// best we can
int importLenChange = 0;
imports = src.getImports();
if (importsEnd != -1){
ISourceRange last = imports[imports.length - 1].getSourceRange();
importLenChange = last.getOffset() + last.getLength() +
lineDelim.length() - importsEnd;
}else if(imports.length > 0){
ISourceRange first = imports[0].getSourceRange();
ISourceRange last = imports[imports.length - 1].getSourceRange();
importLenChange = last.getOffset() + last.getLength() +
(lineDelim.length() * 2) - first.getOffset();
}
offset += importLenChange;
length -= importLenChange;
JavaUtils.format(src, CodeFormatter.K_COMPILATION_UNIT, offset, length);
}
}
private String getMethodBindingSignature(IMethodBinding binding)
{
return binding.toString().trim()
.replaceAll("\\bjava\\.lang\\.", "")
.replaceAll("\\s+throws\\s+.*", "")
.replaceAll("#RAW", "")
.replaceFirst("\\w+\\s*\\(.*?\\)", getMethodBindingCallSignature(binding));
}
private String getMethodBindingCallSignature(IMethodBinding binding)
{
ITypeBinding[] paramTypes = binding.getParameterTypes();
String[] params = new String[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++){
params[i] = paramTypes[i].getQualifiedName()
.replaceAll("\\bjava\\.lang\\.", "")
.replaceAll("#RAW", "");
}
return binding.getName() + '(' + StringUtils.join(params, ',') + ')';
}
private String getMethodBindingShortCallSignature(IMethodBinding binding)
{
return getMethodBindingCallSignature(binding).replaceAll("<.*?>", "");
}
private int getOffset(IJavaElement element)
throws Exception
{
return ((ISourceReference) element).getSourceRange().getOffset();
}
private IJavaElement getSibling(IType type)
throws Exception
{
IJavaElement sibling = null;
// insert after last method
IMethod[] methods = type.getMethods();
if (methods.length > 0){
sibling = MethodUtils.getMethodAfter(type, methods[methods.length - 1]);
}
// insert before inner classes.
if (sibling == null){
IType[] types = type.getTypes();
// find the first non-enum type.
for (int ii = 0; ii < types.length; ii++){
if(!types[ii].isEnum()){
sibling = types[ii];
break;
}
}
}
return sibling;
}
private IJavaElement getSibling(IType type, int offset)
throws Exception
{
// insert before the offset
IMethod[] methods = type.getMethods();
for (IMethod method : methods) {
if (getOffset(method) >= offset) {
return method;
}
}
// insert before inner classes.
IType[] types = type.getTypes();
// find the first non-enum type.
for (IType subtype : types) {
if (getOffset(subtype) >= offset) {
return subtype;
}
}
return null;
}
}