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}