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}