001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.beans; 018 019import java.beans.PropertyDescriptor; 020import java.lang.reflect.Field; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.List; 024 025import org.springframework.util.ObjectUtils; 026import org.springframework.util.ReflectionUtils; 027import org.springframework.util.StringUtils; 028 029/** 030 * Helper class for calculating property matches, according to a configurable 031 * distance. Provide the list of potential matches and an easy way to generate 032 * an error message. Works for both java bean properties and fields. 033 * 034 * <p>Mainly for use within the framework and in particular the binding facility. 035 * 036 * @author Alef Arendsen 037 * @author Arjen Poutsma 038 * @author Juergen Hoeller 039 * @author Stephane Nicoll 040 * @since 2.0 041 * @see #forProperty(String, Class) 042 * @see #forField(String, Class) 043 */ 044public abstract class PropertyMatches { 045 046 /** Default maximum property distance: 2 */ 047 public static final int DEFAULT_MAX_DISTANCE = 2; 048 049 050 // Static factory methods 051 052 /** 053 * Create PropertyMatches for the given bean property. 054 * @param propertyName the name of the property to find possible matches for 055 * @param beanClass the bean class to search for matches 056 */ 057 public static PropertyMatches forProperty(String propertyName, Class<?> beanClass) { 058 return forProperty(propertyName, beanClass, DEFAULT_MAX_DISTANCE); 059 } 060 061 /** 062 * Create PropertyMatches for the given bean property. 063 * @param propertyName the name of the property to find possible matches for 064 * @param beanClass the bean class to search for matches 065 * @param maxDistance the maximum property distance allowed for matches 066 */ 067 public static PropertyMatches forProperty(String propertyName, Class<?> beanClass, int maxDistance) { 068 return new BeanPropertyMatches(propertyName, beanClass, maxDistance); 069 } 070 071 /** 072 * Create PropertyMatches for the given field property. 073 * @param propertyName the name of the field to find possible matches for 074 * @param beanClass the bean class to search for matches 075 */ 076 public static PropertyMatches forField(String propertyName, Class<?> beanClass) { 077 return forField(propertyName, beanClass, DEFAULT_MAX_DISTANCE); 078 } 079 080 /** 081 * Create PropertyMatches for the given field property. 082 * @param propertyName the name of the field to find possible matches for 083 * @param beanClass the bean class to search for matches 084 * @param maxDistance the maximum property distance allowed for matches 085 */ 086 public static PropertyMatches forField(String propertyName, Class<?> beanClass, int maxDistance) { 087 return new FieldPropertyMatches(propertyName, beanClass, maxDistance); 088 } 089 090 091 // Instance state 092 093 private final String propertyName; 094 095 private String[] possibleMatches; 096 097 098 /** 099 * Create a new PropertyMatches instance for the given property and possible matches. 100 */ 101 private PropertyMatches(String propertyName, String[] possibleMatches) { 102 this.propertyName = propertyName; 103 this.possibleMatches = possibleMatches; 104 } 105 106 107 /** 108 * Return the name of the requested property. 109 */ 110 public String getPropertyName() { 111 return this.propertyName; 112 } 113 114 /** 115 * Return the calculated possible matches. 116 */ 117 public String[] getPossibleMatches() { 118 return this.possibleMatches; 119 } 120 121 /** 122 * Build an error message for the given invalid property name, 123 * indicating the possible property matches. 124 */ 125 public abstract String buildErrorMessage(); 126 127 128 // Implementation support for subclasses 129 130 protected void appendHintMessage(StringBuilder msg) { 131 msg.append("Did you mean "); 132 for (int i = 0; i < this.possibleMatches.length; i++) { 133 msg.append('\''); 134 msg.append(this.possibleMatches[i]); 135 if (i < this.possibleMatches.length - 2) { 136 msg.append("', "); 137 } 138 else if (i == this.possibleMatches.length - 2) { 139 msg.append("', or "); 140 } 141 } 142 msg.append("'?"); 143 } 144 145 /** 146 * Calculate the distance between the given two Strings 147 * according to the Levenshtein algorithm. 148 * @param s1 the first String 149 * @param s2 the second String 150 * @return the distance value 151 */ 152 private static int calculateStringDistance(String s1, String s2) { 153 if (s1.isEmpty()) { 154 return s2.length(); 155 } 156 if (s2.isEmpty()) { 157 return s1.length(); 158 } 159 160 int[][] d = new int[s1.length() + 1][s2.length() + 1]; 161 for (int i = 0; i <= s1.length(); i++) { 162 d[i][0] = i; 163 } 164 for (int j = 0; j <= s2.length(); j++) { 165 d[0][j] = j; 166 } 167 168 for (int i = 1; i <= s1.length(); i++) { 169 char c1 = s1.charAt(i - 1); 170 for (int j = 1; j <= s2.length(); j++) { 171 int cost; 172 char c2 = s2.charAt(j - 1); 173 if (c1 == c2) { 174 cost = 0; 175 } 176 else { 177 cost = 1; 178 } 179 d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + cost); 180 } 181 } 182 183 return d[s1.length()][s2.length()]; 184 } 185 186 187 // Concrete subclasses 188 189 private static class BeanPropertyMatches extends PropertyMatches { 190 191 public BeanPropertyMatches(String propertyName, Class<?> beanClass, int maxDistance) { 192 super(propertyName, 193 calculateMatches(propertyName, BeanUtils.getPropertyDescriptors(beanClass), maxDistance)); 194 } 195 196 /** 197 * Generate possible property alternatives for the given property and class. 198 * Internally uses the {@code getStringDistance} method, which in turn uses 199 * the Levenshtein algorithm to determine the distance between two Strings. 200 * @param descriptors the JavaBeans property descriptors to search 201 * @param maxDistance the maximum distance to accept 202 */ 203 private static String[] calculateMatches(String name, PropertyDescriptor[] descriptors, int maxDistance) { 204 List<String> candidates = new ArrayList<String>(); 205 for (PropertyDescriptor pd : descriptors) { 206 if (pd.getWriteMethod() != null) { 207 String possibleAlternative = pd.getName(); 208 if (calculateStringDistance(name, possibleAlternative) <= maxDistance) { 209 candidates.add(possibleAlternative); 210 } 211 } 212 } 213 Collections.sort(candidates); 214 return StringUtils.toStringArray(candidates); 215 } 216 217 @Override 218 public String buildErrorMessage() { 219 StringBuilder msg = new StringBuilder(160); 220 msg.append("Bean property '").append(getPropertyName()).append( 221 "' is not writable or has an invalid setter method. "); 222 if (!ObjectUtils.isEmpty(getPossibleMatches())) { 223 appendHintMessage(msg); 224 } 225 else { 226 msg.append("Does the parameter type of the setter match the return type of the getter?"); 227 } 228 return msg.toString(); 229 } 230 } 231 232 233 private static class FieldPropertyMatches extends PropertyMatches { 234 235 public FieldPropertyMatches(String propertyName, Class<?> beanClass, int maxDistance) { 236 super(propertyName, calculateMatches(propertyName, beanClass, maxDistance)); 237 } 238 239 private static String[] calculateMatches(final String name, Class<?> clazz, final int maxDistance) { 240 final List<String> candidates = new ArrayList<String>(); 241 ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() { 242 @Override 243 public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { 244 String possibleAlternative = field.getName(); 245 if (calculateStringDistance(name, possibleAlternative) <= maxDistance) { 246 candidates.add(possibleAlternative); 247 } 248 } 249 }); 250 Collections.sort(candidates); 251 return StringUtils.toStringArray(candidates); 252 } 253 254 @Override 255 public String buildErrorMessage() { 256 StringBuilder msg = new StringBuilder(80); 257 msg.append("Bean property '").append(getPropertyName()).append("' has no matching field."); 258 if (!ObjectUtils.isEmpty(getPossibleMatches())) { 259 msg.append(' '); 260 appendHintMessage(msg); 261 } 262 return msg.toString(); 263 } 264 } 265 266}