| /******************************************************************************* |
| * Copyright (c) 2008, 2019 Mateusz Matela and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Mateusz Matela <mateusz.matela@gmail.com> - [code manipulation] [dcr] toString() builder wizard - https://bugs.eclipse.org/bugs/show_bug.cgi?id=26070 |
| * Mateusz Matela <mateusz.matela@gmail.com> - [toString] finish toString() builder wizard - https://bugs.eclipse.org/bugs/show_bug.cgi?id=267710 |
| * Red Hat Inc. - moved to jdt.core.manipulation |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.corext.codemanipulation.tostringgeneration; |
| |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.core.runtime.CoreException; |
| |
| import org.eclipse.ltk.core.refactoring.RefactoringStatus; |
| |
| import org.eclipse.jdt.core.Flags; |
| import org.eclipse.jdt.core.IJavaProject; |
| import org.eclipse.jdt.core.IMethod; |
| import org.eclipse.jdt.core.IType; |
| import org.eclipse.jdt.core.JavaModelException; |
| import org.eclipse.jdt.core.NamingConventions; |
| import org.eclipse.jdt.core.dom.ASTNode; |
| import org.eclipse.jdt.core.dom.ClassInstanceCreation; |
| import org.eclipse.jdt.core.dom.Expression; |
| import org.eclipse.jdt.core.dom.ITypeBinding; |
| import org.eclipse.jdt.core.dom.IfStatement; |
| import org.eclipse.jdt.core.dom.InfixExpression.Operator; |
| import org.eclipse.jdt.core.dom.MethodDeclaration; |
| import org.eclipse.jdt.core.dom.MethodInvocation; |
| import org.eclipse.jdt.core.dom.Name; |
| import org.eclipse.jdt.core.dom.ReturnStatement; |
| import org.eclipse.jdt.core.dom.StringLiteral; |
| import org.eclipse.jdt.core.dom.VariableDeclarationFragment; |
| import org.eclipse.jdt.core.dom.VariableDeclarationStatement; |
| |
| import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationMessages; |
| import org.eclipse.jdt.internal.corext.util.JavaModelUtil; |
| |
| |
| /** |
| * <p> |
| * Implementation of <code>AbstractToStringGenerator</code> that creates <code>toString()</code> |
| * method using an external library. The library must supply a string builder, that is a class that |
| * fulfills the following requirements: |
| * <ul> |
| * <li>Provides a constructor taking single Object</li> |
| * <li>Provides methods for appending objects. There may be many such methods (with the same name, |
| * for example <code>append(...)</code>), but there must be at least a version that takes single |
| * Object or an Object and a String (in any order). These methods should return builder object, |
| * otherwise generator will not be able to make call chains.</li> |
| * <li>Provides a result method (usually <code>toString()</code>), that is a method that takes no |
| * arguments and returns a String</li> |
| * </ul> |
| * </p> |
| * <p> |
| * Generated methods look like this: |
| * |
| * <pre> |
| * public String toString() { |
| * ExternalBuilder builder= new ExternalBuilder(); |
| * builder.append("field1", field1); |
| * builder.append("field2", field2); |
| * return builder.toString(); |
| * } |
| * </pre> |
| * |
| * </p> |
| * |
| * @since 3.5 |
| */ |
| public class CustomBuilderGenerator extends AbstractToStringGenerator { |
| |
| private final List<String> primitiveTypes= Arrays.asList(new String[] { "byte", "short", "char", "int", "long", "float", "double", "boolean" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ |
| |
| private final String[] wrapperTypes= new String[] { "java.lang.Byte", "java.lang.Short", "java.lang.Character", "java.lang.Integer", "java.lang.Long", "java.lang.Float", "java.lang.Double", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ |
| "java.lang.Boolean" }; //$NON-NLS-1$ |
| |
| /** |
| * true, if the last expression created with {@link #createAppendMethodForMember(Object)} |
| * returns builder type and therefore can be used to chain calls |
| **/ |
| private boolean canChainLastAppendCall; |
| |
| /** |
| * Class for storing information about versions of append method in the builder class that can |
| * be used for different member types |
| */ |
| private class AppendMethodInformation { |
| /** |
| * Type of method with respect to taken parameter types. Possible values: |
| * <ol> |
| * <li>method takes one type parameter</li> |
| * <li>method takes one type parameter and one string</li> |
| * <li>method takes one string and one type parameter</li> |
| * </ol> |
| */ |
| public int methodType; |
| |
| /** |
| * true if method returns the builder class object so it can be used to form chains of calls |
| **/ |
| public boolean returnsBuilder; |
| } |
| |
| /** |
| * Information about versions of append method in the builder type |
| * |
| * key: String - fully qualified name of a member type |
| * |
| * value: {@link AppendMethodInformation} - information about corresponding method |
| */ |
| private HashMap<String, AppendMethodInformation> appendMethodSpecificTypes= new HashMap<>(); |
| |
| @Override |
| public RefactoringStatus checkConditions() { |
| RefactoringStatus status= super.checkConditions(); |
| if (fContext.isCustomArray() || fContext.isLimitItems()) |
| status.addWarning(CodeGenerationMessages.GenerateToStringOperation_warning_no_arrays_collections_with_this_style); |
| return status; |
| } |
| |
| @Override |
| protected void addElement(Object element) { |
| } |
| |
| @Override |
| protected void initialize() { |
| super.initialize(); |
| |
| fillAppendMethodsMap(); |
| |
| tidyAppendsMethodsMap(); |
| } |
| |
| @Override |
| public MethodDeclaration generateToStringMethod() throws CoreException { |
| initialize(); |
| |
| //ToStringBuilder builder= new ToStringBuilder(this); |
| String builderVariableName= createNameSuggestion(getContext().getCustomBuilderVariableName(), NamingConventions.VK_LOCAL); |
| VariableDeclarationFragment fragment= fAst.newVariableDeclarationFragment(); |
| fragment.setName(fAst.newSimpleName(builderVariableName)); |
| ClassInstanceCreation classInstance= fAst.newClassInstanceCreation(); |
| Name typeName= addImport(getContext().getCustomBuilderClass()); |
| classInstance.setType(fAst.newSimpleType(typeName)); |
| classInstance.arguments().add(fAst.newThisExpression()); |
| fragment.setInitializer(classInstance); |
| VariableDeclarationStatement vStatement= fAst.newVariableDeclarationStatement(fragment); |
| vStatement.setType(fAst.newSimpleType((Name)ASTNode.copySubtree(fAst, typeName))); |
| toStringMethod.getBody().statements().add(vStatement); |
| |
| /* expression for accumulating chained calls */ |
| Expression expression= null; |
| |
| for (int i= 0; i < getContext().getSelectedMembers().length; i++) { |
| //builder.append("member", member); |
| MethodInvocation appendInvocation= createAppendMethodForMember(getContext().getSelectedMembers()[i]); |
| if (getContext().isSkipNulls() && !getMemberType(getContext().getSelectedMembers()[i]).isPrimitive()) { |
| if (expression != null) { |
| toStringMethod.getBody().statements().add(fAst.newExpressionStatement(expression)); |
| expression= null; |
| } |
| appendInvocation.setExpression(fAst.newSimpleName(builderVariableName)); |
| IfStatement ifStatement= fAst.newIfStatement(); |
| ifStatement.setExpression(createInfixExpression(createMemberAccessExpression(getContext().getSelectedMembers()[i], true, true), Operator.NOT_EQUALS, fAst.newNullLiteral())); |
| ifStatement.setThenStatement(createOneStatementBlock(appendInvocation)); |
| toStringMethod.getBody().statements().add(ifStatement); |
| } else { |
| if (expression != null) { |
| appendInvocation.setExpression(expression); |
| } else { |
| appendInvocation.setExpression(fAst.newSimpleName(builderVariableName)); |
| } |
| if (getContext().isCustomBuilderChainedCalls() && canChainLastAppendCall) { |
| expression= appendInvocation; |
| } else { |
| expression= null; |
| toStringMethod.getBody().statements().add(fAst.newExpressionStatement(appendInvocation)); |
| } |
| } |
| } |
| |
| if (expression != null) { |
| toStringMethod.getBody().statements().add(fAst.newExpressionStatement(expression)); |
| } |
| // return builder.toString(); |
| ReturnStatement rStatement= fAst.newReturnStatement(); |
| rStatement.setExpression(createMethodInvocation(builderVariableName, getContext().getCustomBuilderResultMethod(), null)); |
| toStringMethod.getBody().statements().add(rStatement); |
| |
| complete(); |
| |
| return toStringMethod; |
| } |
| |
| /** |
| * Searches through methods with proper name and for each argument type remembers the best |
| * option |
| */ |
| private void fillAppendMethodsMap() { |
| try { |
| IJavaProject javaProject= getContext().getTypeBinding().getJavaElement().getJavaProject(); |
| IType type= javaProject.findType(getContext().getCustomBuilderClass()); |
| IType[] types= type.newSupertypeHierarchy(null).getAllClasses(); |
| for (int i= 0; i < types.length; i++) { |
| IMethod[] methods= types[i].getMethods(); |
| for (int j= 0; j < methods.length; j++) { |
| if (!Flags.isPublic(methods[j].getFlags()) || !methods[j].getElementName().equals(getContext().getCustomBuilderAppendMethod())) |
| continue; |
| String[] parameterTypes= methods[j].getParameterTypes(); |
| AppendMethodInformation appendMethodInformation= new AppendMethodInformation(); |
| String specyficType; |
| switch (parameterTypes.length) { |
| case 1: |
| specyficType= JavaModelUtil.getResolvedTypeName(parameterTypes[0], types[i]); |
| appendMethodInformation.methodType= 1; |
| break; |
| case 2: |
| String resolvedParameterTypeName1= JavaModelUtil.getResolvedTypeName(parameterTypes[0], types[i]); |
| String resolvedParameterTypeName2= JavaModelUtil.getResolvedTypeName(parameterTypes[1], types[i]); |
| if (resolvedParameterTypeName1.equals("java.lang.String")) {//$NON-NLS-1$ |
| specyficType= resolvedParameterTypeName2; |
| appendMethodInformation.methodType= 3; |
| } else if (resolvedParameterTypeName2.equals("java.lang.String")) {//$NON-NLS-1$ |
| specyficType= resolvedParameterTypeName1; |
| appendMethodInformation.methodType= 2; |
| } else |
| continue; |
| break; |
| default: |
| continue; |
| } |
| |
| String returnTypeName= JavaModelUtil.getResolvedTypeName(methods[j].getReturnType(), types[i]); |
| IType returnType= javaProject.findType(returnTypeName); |
| appendMethodInformation.returnsBuilder= (returnType != null) && returnType.newSupertypeHierarchy(null).contains(type); |
| |
| AppendMethodInformation oldAMI= appendMethodSpecificTypes.get(specyficType); |
| if (oldAMI == null || oldAMI.methodType < appendMethodInformation.methodType) { |
| appendMethodSpecificTypes.put(specyficType, appendMethodInformation); |
| } |
| } |
| } |
| } catch (JavaModelException e) { |
| throw new RuntimeException("couldn't initialize custom toString() builder generator", e); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Removes information about types from {@link #appendMethodSpecificTypes} if their |
| * parametersType is worse than for java.lang.Object. |
| */ |
| private void tidyAppendsMethodsMap() { |
| int objectParametersType= appendMethodSpecificTypes.get("java.lang.Object").methodType; //$NON-NLS-1$ |
| Set<Map.Entry<String, AppendMethodInformation>> entrySet= appendMethodSpecificTypes.entrySet(); |
| for (Iterator<Map.Entry<String, AppendMethodInformation>> iterator= entrySet.iterator(); iterator.hasNext();) { |
| Map.Entry<String, AppendMethodInformation> entry= iterator.next(); |
| if (entry.getValue().methodType < objectParametersType) { |
| iterator.remove(); |
| } |
| } |
| } |
| |
| private MethodInvocation createAppendMethodForMember(Object member) { |
| ITypeBinding memberType= getMemberType(member); |
| String memberTypeName= memberType.getQualifiedName(); |
| |
| Expression memberAccessExpression= null; |
| |
| AppendMethodInformation ami= appendMethodSpecificTypes.get(memberTypeName); |
| if (ami == null && memberType.isPrimitive()) { |
| memberTypeName= wrapperTypes[primitiveTypes.indexOf(memberTypeName)]; |
| memberType= fAst.resolveWellKnownType(memberTypeName); |
| ami= appendMethodSpecificTypes.get(memberTypeName); |
| if (!getContext().is50orHigher()) { |
| ClassInstanceCreation classInstance= fAst.newClassInstanceCreation(); |
| classInstance.setType(fAst.newSimpleType(addImport(memberTypeName))); |
| classInstance.arguments().add(createMemberAccessExpression(member, true, true)); |
| memberAccessExpression= classInstance; |
| } |
| } |
| while (ami == null) { |
| memberType= memberType.getSuperclass(); |
| if (memberType != null) |
| memberTypeName= memberType.getQualifiedName(); |
| else |
| memberTypeName= "java.lang.Object"; //$NON-NLS-1$ |
| ami= appendMethodSpecificTypes.get(memberTypeName); |
| } |
| |
| if (memberAccessExpression == null) { |
| memberAccessExpression= createMemberAccessExpression(member, false, getContext().isSkipNulls()); |
| } |
| |
| MethodInvocation appendInvocation= fAst.newMethodInvocation(); |
| appendInvocation.setName(fAst.newSimpleName(getContext().getCustomBuilderAppendMethod())); |
| if (ami.methodType == 1 || ami.methodType == 2) { |
| appendInvocation.arguments().add(memberAccessExpression); |
| } |
| if (ami.methodType == 2 || ami.methodType == 3) { |
| StringLiteral literal= fAst.newStringLiteral(); |
| literal.setLiteralValue(getMemberName(member, ToStringTemplateParser.MEMBER_NAME_PARENTHESIS_VARIABLE)); |
| appendInvocation.arguments().add(literal); |
| } |
| if (ami.methodType == 3) { |
| appendInvocation.arguments().add(memberAccessExpression); |
| } |
| |
| canChainLastAppendCall= ami.returnsBuilder; |
| |
| return appendInvocation; |
| } |
| } |