1 /******************************************************************************* 2 * Copyright (c) 2005, 2019 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 * Mateusz Matela <mateusz.matela@gmail.com> - [code manipulation] [dcr] toString() builder wizard - https://bugs.eclipse.org/bugs/show_bug.cgi?id=26070 14 * Pierre-Yves B. <pyvesdev@gmail.com> - Check whether enclosing instance implements hashCode and equals - https://bugs.eclipse.org/539900 15 * Pierre-Yves B. <pyvesdev@gmail.com> - Allow hashCode and equals generation when no fields but a super/enclosing class that implements them - https://bugs.eclipse.org/539901 16 *******************************************************************************/ 17 package org.eclipse.jdt.ui.actions; 18 19 import java.util.ArrayList; 20 import java.util.Arrays; 21 import java.util.List; 22 23 import org.eclipse.swt.widgets.Shell; 24 25 import org.eclipse.core.resources.IWorkspaceRunnable; 26 27 import org.eclipse.ui.IWorkbenchSite; 28 import org.eclipse.ui.PlatformUI; 29 30 import org.eclipse.ltk.core.refactoring.RefactoringStatus; 31 32 import org.eclipse.jdt.core.IJavaElement; 33 import org.eclipse.jdt.core.IType; 34 import org.eclipse.jdt.core.JavaModelException; 35 import org.eclipse.jdt.core.dom.IMethodBinding; 36 import org.eclipse.jdt.core.dom.ITypeBinding; 37 import org.eclipse.jdt.core.dom.IVariableBinding; 38 import org.eclipse.jdt.core.dom.Modifier; 39 40 import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels; 41 import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationSettings; 42 import org.eclipse.jdt.internal.corext.codemanipulation.GenerateHashCodeEqualsOperation; 43 import org.eclipse.jdt.internal.corext.dom.TypeRules; 44 import org.eclipse.jdt.internal.corext.util.Messages; 45 46 import org.eclipse.jdt.ui.JavaElementLabels; 47 48 import org.eclipse.jdt.internal.ui.IJavaHelpContextIds; 49 import org.eclipse.jdt.internal.ui.actions.ActionMessages; 50 import org.eclipse.jdt.internal.ui.actions.SelectionConverter; 51 import org.eclipse.jdt.internal.ui.dialogs.GenerateHashCodeEqualsDialog; 52 import org.eclipse.jdt.internal.ui.dialogs.SourceActionDialog; 53 import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor; 54 import org.eclipse.jdt.internal.ui.viewsupport.BindingLabelProvider; 55 56 /** 57 * Adds method implementations for 58 * <code>{@link java.lang.Object#equals(java.lang.Object)}</code> and 59 * <code>{@link java.lang.Object#hashCode()}</code>. The action opens a 60 * dialog from which the user can choose the fields to be considered. 61 * <p> 62 * Will open the parent compilation unit in a Java editor. The result is 63 * unsaved, so the user can decide if the changes are acceptable. 64 * <p> 65 * The action is applicable to structured selections containing elements of type 66 * {@link org.eclipse.jdt.core.IType}. 67 * 68 * <p> 69 * This class may be instantiated; it is not intended to be subclassed. 70 * </p> 71 * 72 * @since 3.2 73 */ 74 public final class GenerateHashCodeEqualsAction extends GenerateMethodAbstractAction { 75 76 private static final String METHODNAME_HASH_CODE= "hashCode"; //$NON-NLS-1$ 77 private static final String METHODNAME_EQUALS= "equals"; //$NON-NLS-1$ 78 79 private class HashCodeEqualsInfo { 80 81 public boolean foundHashCode= false; 82 83 public boolean foundEquals= false; 84 85 public boolean foundFinalHashCode= false; 86 87 public boolean foundFinalEquals= false; 88 } 89 90 private class HashCodeEqualsGenerationSettings extends CodeGenerationSettings { 91 public boolean useInstanceOf= false; 92 public boolean useBlocks= false; 93 public boolean useJ7HashEquals= false; 94 } 95 96 private List<IVariableBinding> allFields; 97 private List<IVariableBinding> selectedFields; 98 99 private ArrayList<ITypeBinding> alreadyCheckedMemberTypes; 100 101 private HashCodeEqualsInfo superClassInfo= new HashCodeEqualsInfo(); 102 private HashCodeEqualsInfo enclosingClassInfo= new HashCodeEqualsInfo(); 103 104 /** 105 * Note: This constructor is for internal use only. Clients should not call 106 * this constructor. 107 * 108 * @param editor the compilation unit editor 109 * 110 * @noreference This constructor is not intended to be referenced by clients. 111 */ GenerateHashCodeEqualsAction(final CompilationUnitEditor editor)112 public GenerateHashCodeEqualsAction(final CompilationUnitEditor editor) { 113 this(editor.getEditorSite()); 114 fEditor= editor; 115 setEnabled( (fEditor != null && SelectionConverter.canOperateOn(fEditor))); 116 } 117 118 /** 119 * Creates a new generate hashCode equals action. 120 * <p> 121 * The action requires that the selection provided by the site's selection 122 * provider is of type 123 * {@link org.eclipse.jface.viewers.IStructuredSelection}. 124 * 125 * @param site the workbench site providing context information for this 126 * action 127 */ GenerateHashCodeEqualsAction(final IWorkbenchSite site)128 public GenerateHashCodeEqualsAction(final IWorkbenchSite site) { 129 super(site); 130 setText(ActionMessages.GenerateHashCodeEqualsAction_label); 131 setDescription(ActionMessages.GenerateHashCodeEqualsAction_description); 132 setToolTipText(ActionMessages.GenerateHashCodeEqualsAction_tooltip); 133 PlatformUI.getWorkbench().getHelpSystem().setHelp(this, IJavaHelpContextIds.GENERATE_HASHCODE_EQUALS_ACTION); 134 } 135 136 @Override isMethodAlreadyImplemented(ITypeBinding typeBinding)137 boolean isMethodAlreadyImplemented(ITypeBinding typeBinding) { 138 HashCodeEqualsInfo info= getTypeInfo(typeBinding, false); 139 return (info.foundEquals || info.foundHashCode); 140 } 141 getTypeInfo(ITypeBinding someType, boolean checkSuperclasses)142 private HashCodeEqualsInfo getTypeInfo(ITypeBinding someType, boolean checkSuperclasses) { 143 HashCodeEqualsInfo info= new HashCodeEqualsInfo(); 144 if (someType.isTypeVariable()) { 145 someType= someType.getErasure(); 146 } 147 148 while (true) { 149 for (IMethodBinding declaredMethod : someType.getDeclaredMethods()) { 150 if (declaredMethod.getName().equals(METHODNAME_EQUALS)) { 151 ITypeBinding[] b= declaredMethod.getParameterTypes(); 152 if ((b.length == 1) && (b[0].getQualifiedName().equals("java.lang.Object"))) { //$NON-NLS-1$ 153 info.foundEquals= true; 154 if (Modifier.isFinal(declaredMethod.getModifiers())) { 155 info.foundFinalEquals= true; 156 } 157 } 158 } 159 if (declaredMethod.getName().equals(METHODNAME_HASH_CODE) && declaredMethod.getParameterTypes().length == 0) { 160 info.foundHashCode= true; 161 if (Modifier.isFinal(declaredMethod.getModifiers())) { 162 info.foundFinalHashCode= true; 163 } 164 } 165 if (info.foundEquals && info.foundHashCode) 166 break; 167 } 168 if (checkSuperclasses) { 169 someType= someType.getSuperclass(); 170 if (someType == null || TypeRules.isJavaLangObject(someType)) { 171 break; 172 } 173 } else { 174 break; 175 } 176 } 177 178 return info; 179 } 180 checkHashCodeEqualsExists(ITypeBinding someType, HashCodeEqualsInfo info, boolean checkFinalMethods, String concreteTypeWarning)181 private RefactoringStatus checkHashCodeEqualsExists(ITypeBinding someType, HashCodeEqualsInfo info, boolean checkFinalMethods, String concreteTypeWarning) { 182 183 RefactoringStatus status= new RefactoringStatus(); 184 185 String concreteMethWarning= (someType.isInterface() || Modifier.isAbstract(someType.getModifiers())) 186 ? ActionMessages.GenerateHashCodeEqualsAction_interface_does_not_declare_hashCode_equals_error 187 : ActionMessages.GenerateHashCodeEqualsAction_type_does_not_implement_hashCode_equals_error; 188 String concreteHCEWarning= null; 189 190 if (!info.foundEquals && (!info.foundHashCode)) 191 concreteHCEWarning= ActionMessages.GenerateHashCodeEqualsAction_equals_and_hashCode; 192 else if (!info.foundEquals) 193 concreteHCEWarning= ActionMessages.GenerateHashCodeEqualsAction_equals; 194 else if (!info.foundHashCode) 195 concreteHCEWarning= ActionMessages.GenerateHashCodeEqualsAction_hashCode; 196 197 if (!info.foundEquals || !info.foundHashCode) 198 status.addWarning(Messages.format(concreteMethWarning, new String[] { 199 Messages.format(concreteTypeWarning, BindingLabelProvider.getBindingLabel(someType, JavaElementLabels.ALL_FULLY_QUALIFIED)), concreteHCEWarning }), 200 createRefactoringStatusContext(someType.getJavaElement())); 201 202 if (checkFinalMethods && (info.foundFinalEquals || info.foundFinalHashCode)) { 203 status.addError(Messages.format(ActionMessages.GenerateMethodAbstractAction_final_method_in_superclass_error, new String[] { 204 Messages.format(concreteTypeWarning, BasicElementLabels.getJavaElementName(someType.getQualifiedName())), ActionMessages.GenerateHashCodeEqualsAction_hashcode_or_equals }), 205 createRefactoringStatusContext(someType.getJavaElement())); 206 } 207 208 return status; 209 } 210 211 @Override createSettings(IType type, SourceActionDialog dialog)212 CodeGenerationSettings createSettings(IType type, SourceActionDialog dialog) { 213 HashCodeEqualsGenerationSettings settings= new HashCodeEqualsGenerationSettings(); 214 super.createSettings(type, dialog).setSettings(settings); 215 settings.createComments= dialog.getGenerateComment(); 216 GenerateHashCodeEqualsDialog generateHashCodeEqualsDialog= (GenerateHashCodeEqualsDialog)dialog; 217 settings.useInstanceOf= generateHashCodeEqualsDialog.isUseInstanceOf(); 218 settings.useBlocks= generateHashCodeEqualsDialog.isUseBlocks(); 219 settings.useJ7HashEquals= generateHashCodeEqualsDialog.isUseJ7HashEquals(); 220 return settings; 221 } 222 223 @Override initialize(IType type)224 void initialize(IType type) throws JavaModelException { 225 super.initialize(type); 226 alreadyCheckedMemberTypes= new ArrayList<>(); 227 } 228 229 @Override getAlreadyImplementedErrorMethodName()230 String getAlreadyImplementedErrorMethodName() { 231 return ActionMessages.GenerateHashCodeEqualsAction_hashcode_or_equals; 232 } 233 234 @Override generateCandidates()235 boolean generateCandidates() { 236 allFields= new ArrayList<>(); 237 selectedFields= new ArrayList<>(); 238 for (IVariableBinding candidateField : fTypeBinding.getDeclaredFields()) { 239 if (!Modifier.isStatic(candidateField.getModifiers())) { 240 allFields.add(candidateField); 241 if (!Modifier.isTransient(candidateField.getModifiers())) { 242 selectedFields.add(candidateField); 243 } 244 } 245 } 246 247 ITypeBinding superclass= fTypeBinding.getSuperclass(); 248 if (!"java.lang.Object".equals(superclass.getQualifiedName())) //$NON-NLS-1$ 249 superClassInfo= getTypeInfo(superclass, true); 250 251 if (fTypeBinding.isMember() && !Modifier.isStatic(fTypeBinding.getModifiers())) 252 enclosingClassInfo= getTypeInfo(fTypeBinding.getDeclaringClass(), true); 253 254 return !allFields.isEmpty() || foundHashCodeOrEqualsInEnclosingOrSuperClass(); 255 } 256 257 @Override createDialog(Shell shell, IType type)258 SourceActionDialog createDialog(Shell shell, IType type) throws JavaModelException { 259 IVariableBinding[] allFieldBindings= allFields.toArray(new IVariableBinding[0]); 260 IVariableBinding[] selectedFieldBindings= selectedFields.toArray(new IVariableBinding[0]); 261 return new GenerateHashCodeEqualsDialog(shell, fEditor, type, allFieldBindings, selectedFieldBindings); 262 } 263 264 @Override checkSuperClass(ITypeBinding superClass)265 RefactoringStatus checkSuperClass(ITypeBinding superClass) { 266 return checkHashCodeEqualsExists(superClass, superClassInfo, true, ActionMessages.GenerateMethodAbstractAction_super_class); 267 } 268 269 @Override checkEnclosingClass(ITypeBinding enclosingClass)270 RefactoringStatus checkEnclosingClass(ITypeBinding enclosingClass) { 271 return checkHashCodeEqualsExists(enclosingClass, enclosingClassInfo, false, ActionMessages.GenerateMethodAbstractAction_enclosing_class); 272 } 273 274 @Override checkGeneralConditions(IType type, CodeGenerationSettings settings, Object[] selected)275 RefactoringStatus checkGeneralConditions(IType type, CodeGenerationSettings settings, Object[] selected) { 276 return new RefactoringStatus(); 277 } 278 279 @Override checkMember(Object memberBinding)280 RefactoringStatus checkMember(Object memberBinding) { 281 RefactoringStatus status= new RefactoringStatus(); 282 IVariableBinding variableBinding= (IVariableBinding)memberBinding; 283 ITypeBinding fieldsType= variableBinding.getType(); 284 if (fieldsType.isArray()) 285 fieldsType= fieldsType.getElementType(); 286 if (!fieldsType.isPrimitive() && !fieldsType.isEnum() && !alreadyCheckedMemberTypes.contains(fieldsType) && !fieldsType.equals(fTypeBinding)) { 287 status.merge(checkHashCodeEqualsExists(fieldsType, getTypeInfo(fieldsType, true), false, ActionMessages.GenerateHashCodeEqualsAction_field_type)); 288 alreadyCheckedMemberTypes.add(fieldsType); 289 } 290 if (Modifier.isTransient(variableBinding.getModifiers())) 291 status.addWarning(Messages.format(ActionMessages.GenerateHashCodeEqualsAction_transient_field_included_error, BasicElementLabels.getJavaElementName(variableBinding.getName())), 292 createRefactoringStatusContext(variableBinding.getJavaElement())); 293 return status; 294 } 295 296 @Override createOperation(Object[] selectedBindings, CodeGenerationSettings settings, boolean regenerate, IJavaElement type, IJavaElement elementPosition)297 IWorkspaceRunnable createOperation(Object[] selectedBindings, CodeGenerationSettings settings, boolean regenerate, IJavaElement type, IJavaElement elementPosition) { 298 final IVariableBinding[] selectedVariableBindings= Arrays.asList(selectedBindings).toArray(new IVariableBinding[0]); 299 HashCodeEqualsGenerationSettings hashCodeEqualsGenerationSettings= (HashCodeEqualsGenerationSettings)settings; 300 GenerateHashCodeEqualsOperation operation= new GenerateHashCodeEqualsOperation(fTypeBinding, selectedVariableBindings, fUnit, elementPosition, settings, 301 hashCodeEqualsGenerationSettings.useInstanceOf, hashCodeEqualsGenerationSettings.useJ7HashEquals, regenerate, true, false); 302 operation.setUseBlocksForThen(hashCodeEqualsGenerationSettings.useBlocks); 303 return operation; 304 } 305 306 @Override getErrorCaption()307 String getErrorCaption() { 308 return ActionMessages.GenerateHashCodeEqualsAction_error_caption; 309 } 310 311 @Override getNoMembersError()312 String getNoMembersError() { 313 return ActionMessages.GenerateHashCodeEqualsAction_no_nonstatic_fields_error; 314 } 315 foundHashCodeOrEqualsInEnclosingOrSuperClass()316 private boolean foundHashCodeOrEqualsInEnclosingOrSuperClass() { 317 return enclosingClassInfo.foundHashCode || enclosingClassInfo.foundEquals 318 || superClassInfo.foundHashCode || superClassInfo.foundEquals; 319 } 320 321 } 322