/** * 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; } }