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