Skip to content

Commit 2478330

Browse files
committed
Add possible matches for field access
DirectFieldAccessor now support richer NotWritablePropertyException content, including dedicated error message and possible matches. Issue: SPR-13053
1 parent 9e8e7aa commit 2478330

File tree

2 files changed

+339
-55
lines changed

2 files changed

+339
-55
lines changed

spring-beans/src/main/java/org/springframework/beans/PropertyMatches.java

Lines changed: 147 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,11 +17,13 @@
1717
package org.springframework.beans;
1818

1919
import java.beans.PropertyDescriptor;
20+
import java.lang.reflect.Field;
2021
import java.util.ArrayList;
2122
import java.util.Collections;
2223
import java.util.List;
2324

2425
import org.springframework.util.ObjectUtils;
26+
import org.springframework.util.ReflectionUtils;
2527
import org.springframework.util.StringUtils;
2628

2729
/**
@@ -31,10 +33,11 @@
3133
* @author Alef Arendsen
3234
* @author Arjen Poutsma
3335
* @author Juergen Hoeller
36+
* @author Stephane Nicoll
3437
* @since 2.0
3538
* @see #forProperty(String, Class)
3639
*/
37-
final class PropertyMatches {
40+
abstract class PropertyMatches {
3841

3942
//---------------------------------------------------------------------
4043
// Static section
@@ -60,7 +63,26 @@ public static PropertyMatches forProperty(String propertyName, Class<?> beanClas
6063
* @param maxDistance the maximum property distance allowed for matches
6164
*/
6265
public static PropertyMatches forProperty(String propertyName, Class<?> beanClass, int maxDistance) {
63-
return new PropertyMatches(propertyName, beanClass, maxDistance);
66+
return new BeanPropertyMatches(propertyName, beanClass, maxDistance);
67+
}
68+
69+
/**
70+
* Create PropertyMatches for the given field property.
71+
* @param propertyName the name of the field to find possible matches for
72+
* @param beanClass the bean class to search for matches
73+
*/
74+
public static PropertyMatches forField(String propertyName, Class<?> beanClass) {
75+
return forField(propertyName, beanClass, DEFAULT_MAX_DISTANCE);
76+
}
77+
78+
/**
79+
* Create PropertyMatches for the given field property.
80+
* @param propertyName the name of the field to find possible matches for
81+
* @param beanClass the bean class to search for matches
82+
* @param maxDistance the maximum property distance allowed for matches
83+
*/
84+
public static PropertyMatches forField(String propertyName, Class<?> beanClass, int maxDistance) {
85+
return new FieldPropertyMatches(propertyName, beanClass, maxDistance);
6486
}
6587

6688

@@ -74,13 +96,19 @@ public static PropertyMatches forProperty(String propertyName, Class<?> beanClas
7496

7597

7698
/**
77-
* Create a new PropertyMatches instance for the given property.
99+
* Create a new PropertyMatches instance for the given property and possible matches.
78100
*/
79-
private PropertyMatches(String propertyName, Class<?> beanClass, int maxDistance) {
101+
private PropertyMatches(String propertyName, String[] possibleMatches) {
80102
this.propertyName = propertyName;
81-
this.possibleMatches = calculateMatches(BeanUtils.getPropertyDescriptors(beanClass), maxDistance);
103+
this.possibleMatches = possibleMatches;
82104
}
83105

106+
/**
107+
* Return the name of the requested property.
108+
*/
109+
public String getPropertyName() {
110+
return propertyName;
111+
}
84112

85113
/**
86114
* Return the calculated possible matches.
@@ -93,54 +121,7 @@ public String[] getPossibleMatches() {
93121
* Build an error message for the given invalid property name,
94122
* indicating the possible property matches.
95123
*/
96-
public String buildErrorMessage() {
97-
StringBuilder msg = new StringBuilder();
98-
msg.append("Bean property '");
99-
msg.append(this.propertyName);
100-
msg.append("' is not writable or has an invalid setter method. ");
101-
102-
if (ObjectUtils.isEmpty(this.possibleMatches)) {
103-
msg.append("Does the parameter type of the setter match the return type of the getter?");
104-
}
105-
else {
106-
msg.append("Did you mean ");
107-
for (int i = 0; i < this.possibleMatches.length; i++) {
108-
msg.append('\'');
109-
msg.append(this.possibleMatches[i]);
110-
if (i < this.possibleMatches.length - 2) {
111-
msg.append("', ");
112-
}
113-
else if (i == this.possibleMatches.length - 2){
114-
msg.append("', or ");
115-
}
116-
}
117-
msg.append("'?");
118-
}
119-
return msg.toString();
120-
}
121-
122-
123-
/**
124-
* Generate possible property alternatives for the given property and
125-
* class. Internally uses the {@code getStringDistance} method, which
126-
* in turn uses the Levenshtein algorithm to determine the distance between
127-
* two Strings.
128-
* @param propertyDescriptors the JavaBeans property descriptors to search
129-
* @param maxDistance the maximum distance to accept
130-
*/
131-
private String[] calculateMatches(PropertyDescriptor[] propertyDescriptors, int maxDistance) {
132-
List<String> candidates = new ArrayList<String>();
133-
for (PropertyDescriptor pd : propertyDescriptors) {
134-
if (pd.getWriteMethod() != null) {
135-
String possibleAlternative = pd.getName();
136-
if (calculateStringDistance(this.propertyName, possibleAlternative) <= maxDistance) {
137-
candidates.add(possibleAlternative);
138-
}
139-
}
140-
}
141-
Collections.sort(candidates);
142-
return StringUtils.toStringArray(candidates);
143-
}
124+
public abstract String buildErrorMessage();
144125

145126
/**
146127
* Calculate the distance between the given two Strings
@@ -149,7 +130,7 @@ private String[] calculateMatches(PropertyDescriptor[] propertyDescriptors, int
149130
* @param s2 the second String
150131
* @return the distance value
151132
*/
152-
private int calculateStringDistance(String s1, String s2) {
133+
private static int calculateStringDistance(String s1, String s2) {
153134
if (s1.length() == 0) {
154135
return s2.length();
155136
}
@@ -184,4 +165,115 @@ private int calculateStringDistance(String s1, String s2) {
184165
return d[s1.length()][s2.length()];
185166
}
186167

168+
private static class BeanPropertyMatches extends PropertyMatches {
169+
170+
private BeanPropertyMatches(String propertyName, Class<?> beanClass, int maxDistance) {
171+
super(propertyName, calculateMatches(propertyName,
172+
BeanUtils.getPropertyDescriptors(beanClass), maxDistance));
173+
}
174+
175+
/**
176+
* Generate possible property alternatives for the given property and
177+
* class. Internally uses the {@code getStringDistance} method, which
178+
* in turn uses the Levenshtein algorithm to determine the distance between
179+
* two Strings.
180+
* @param propertyDescriptors the JavaBeans property descriptors to search
181+
* @param maxDistance the maximum distance to accept
182+
*/
183+
private static String[] calculateMatches(String propertyName, PropertyDescriptor[] propertyDescriptors, int maxDistance) {
184+
List<String> candidates = new ArrayList<String>();
185+
for (PropertyDescriptor pd : propertyDescriptors) {
186+
if (pd.getWriteMethod() != null) {
187+
String possibleAlternative = pd.getName();
188+
if (calculateStringDistance(propertyName, possibleAlternative) <= maxDistance) {
189+
candidates.add(possibleAlternative);
190+
}
191+
}
192+
}
193+
Collections.sort(candidates);
194+
return StringUtils.toStringArray(candidates);
195+
}
196+
197+
198+
@Override
199+
public String buildErrorMessage() {
200+
String propertyName = getPropertyName();
201+
String[] possibleMatches = getPossibleMatches();
202+
StringBuilder msg = new StringBuilder();
203+
msg.append("Bean property '");
204+
msg.append(propertyName);
205+
msg.append("' is not writable or has an invalid setter method. ");
206+
207+
if (ObjectUtils.isEmpty(possibleMatches)) {
208+
msg.append("Does the parameter type of the setter match the return type of the getter?");
209+
}
210+
else {
211+
msg.append("Did you mean ");
212+
for (int i = 0; i < possibleMatches.length; i++) {
213+
msg.append('\'');
214+
msg.append(possibleMatches[i]);
215+
if (i < possibleMatches.length - 2) {
216+
msg.append("', ");
217+
}
218+
else if (i == possibleMatches.length - 2) {
219+
msg.append("', or ");
220+
}
221+
}
222+
msg.append("'?");
223+
}
224+
return msg.toString();
225+
}
226+
227+
}
228+
229+
private static class FieldPropertyMatches extends PropertyMatches {
230+
231+
private FieldPropertyMatches(String propertyName, Class<?> beanClass, int maxDistance) {
232+
super(propertyName, calculateMatches(propertyName, beanClass, maxDistance));
233+
}
234+
235+
private static String[] calculateMatches(final String propertyName, Class<?> beanClass, final int maxDistance) {
236+
final List<String> candidates = new ArrayList<String>();
237+
ReflectionUtils.doWithFields(beanClass, new ReflectionUtils.FieldCallback() {
238+
@Override
239+
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
240+
String possibleAlternative = field.getName();
241+
if (calculateStringDistance(propertyName, possibleAlternative) <= maxDistance) {
242+
candidates.add(possibleAlternative);
243+
}
244+
}
245+
});
246+
Collections.sort(candidates);
247+
return StringUtils.toStringArray(candidates);
248+
}
249+
250+
251+
@Override
252+
public String buildErrorMessage() {
253+
String propertyName = getPropertyName();
254+
String[] possibleMatches = getPossibleMatches();
255+
StringBuilder msg = new StringBuilder();
256+
msg.append("Bean property '");
257+
msg.append(propertyName);
258+
msg.append("' has no matching field. ");
259+
260+
if (!ObjectUtils.isEmpty(possibleMatches)) {
261+
msg.append("Did you mean ");
262+
for (int i = 0; i < possibleMatches.length; i++) {
263+
msg.append('\'');
264+
msg.append(possibleMatches[i]);
265+
if (i < possibleMatches.length - 2) {
266+
msg.append("', ");
267+
}
268+
else if (i == possibleMatches.length - 2) {
269+
msg.append("', or ");
270+
}
271+
}
272+
msg.append("'?");
273+
}
274+
return msg.toString();
275+
}
276+
277+
}
278+
187279
}

0 commit comments

Comments
 (0)