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