001/*
002 * Copyright 2002-2017 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.validation;
018
019import java.io.Serializable;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Set;
025import java.util.StringJoiner;
026
027import org.springframework.lang.Nullable;
028import org.springframework.util.StringUtils;
029
030/**
031 * Default implementation of the {@link MessageCodesResolver} interface.
032 *
033 * <p>Will create two message codes for an object error, in the following order (when
034 * using the {@link Format#PREFIX_ERROR_CODE prefixed}
035 * {@link #setMessageCodeFormatter(MessageCodeFormatter) formatter}):
036 * <ul>
037 * <li>1.: code + "." + object name
038 * <li>2.: code
039 * </ul>
040 *
041 * <p>Will create four message codes for a field specification, in the following order:
042 * <ul>
043 * <li>1.: code + "." + object name + "." + field
044 * <li>2.: code + "." + field
045 * <li>3.: code + "." + field type
046 * <li>4.: code
047 * </ul>
048 *
049 * <p>For example, in case of code "typeMismatch", object name "user", field "age":
050 * <ul>
051 * <li>1. try "typeMismatch.user.age"
052 * <li>2. try "typeMismatch.age"
053 * <li>3. try "typeMismatch.int"
054 * <li>4. try "typeMismatch"
055 * </ul>
056 *
057 * <p>This resolution algorithm thus can be leveraged for example to show
058 * specific messages for binding errors like "required" and "typeMismatch":
059 * <ul>
060 * <li>at the object + field level ("age" field, but only on "user");
061 * <li>at the field level (all "age" fields, no matter which object name);
062 * <li>or at the general level (all fields, on any object).
063 * </ul>
064 *
065 * <p>In case of array, {@link List} or {@link java.util.Map} properties,
066 * both codes for specific elements and for the whole collection are
067 * generated. Assuming a field "name" of an array "groups" in object "user":
068 * <ul>
069 * <li>1. try "typeMismatch.user.groups[0].name"
070 * <li>2. try "typeMismatch.user.groups.name"
071 * <li>3. try "typeMismatch.groups[0].name"
072 * <li>4. try "typeMismatch.groups.name"
073 * <li>5. try "typeMismatch.name"
074 * <li>6. try "typeMismatch.java.lang.String"
075 * <li>7. try "typeMismatch"
076 * </ul>
077 *
078 * <p>By default the {@code errorCode}s will be placed at the beginning of constructed
079 * message strings. The {@link #setMessageCodeFormatter(MessageCodeFormatter)
080 * messageCodeFormatter} property can be used to specify an alternative concatenation
081 * {@link MessageCodeFormatter format}.
082 *
083 * <p>In order to group all codes into a specific category within your resource bundles,
084 * e.g. "validation.typeMismatch.name" instead of the default "typeMismatch.name",
085 * consider specifying a {@link #setPrefix prefix} to be applied.
086 *
087 * @author Juergen Hoeller
088 * @author Phillip Webb
089 * @author Chris Beams
090 * @since 1.0.1
091 */
092@SuppressWarnings("serial")
093public class DefaultMessageCodesResolver implements MessageCodesResolver, Serializable {
094
095        /**
096         * The separator that this implementation uses when resolving message codes.
097         */
098        public static final String CODE_SEPARATOR = ".";
099
100        private static final MessageCodeFormatter DEFAULT_FORMATTER = Format.PREFIX_ERROR_CODE;
101
102
103        private String prefix = "";
104
105        private MessageCodeFormatter formatter = DEFAULT_FORMATTER;
106
107
108        /**
109         * Specify a prefix to be applied to any code built by this resolver.
110         * <p>Default is none. Specify, for example, "validation." to get
111         * error codes like "validation.typeMismatch.name".
112         */
113        public void setPrefix(@Nullable String prefix) {
114                this.prefix = (prefix != null ? prefix : "");
115        }
116
117        /**
118         * Return the prefix to be applied to any code built by this resolver.
119         * <p>Returns an empty String in case of no prefix.
120         */
121        protected String getPrefix() {
122                return this.prefix;
123        }
124
125        /**
126         * Specify the format for message codes built by this resolver.
127         * <p>The default is {@link Format#PREFIX_ERROR_CODE}.
128         * @since 3.2
129         * @see Format
130         */
131        public void setMessageCodeFormatter(@Nullable MessageCodeFormatter formatter) {
132                this.formatter = (formatter != null ? formatter : DEFAULT_FORMATTER);
133        }
134
135
136        @Override
137        public String[] resolveMessageCodes(String errorCode, String objectName) {
138                return resolveMessageCodes(errorCode, objectName, "", null);
139        }
140
141        /**
142         * Build the code list for the given code and field: an
143         * object/field-specific code, a field-specific code, a plain error code.
144         * <p>Arrays, Lists and Maps are resolved both for specific elements and
145         * the whole collection.
146         * <p>See the {@link DefaultMessageCodesResolver class level javadoc} for
147         * details on the generated codes.
148         * @return the list of codes
149         */
150        @Override
151        public String[] resolveMessageCodes(String errorCode, String objectName, String field, @Nullable Class<?> fieldType) {
152                Set<String> codeList = new LinkedHashSet<>();
153                List<String> fieldList = new ArrayList<>();
154                buildFieldList(field, fieldList);
155                addCodes(codeList, errorCode, objectName, fieldList);
156                int dotIndex = field.lastIndexOf('.');
157                if (dotIndex != -1) {
158                        buildFieldList(field.substring(dotIndex + 1), fieldList);
159                }
160                addCodes(codeList, errorCode, null, fieldList);
161                if (fieldType != null) {
162                        addCode(codeList, errorCode, null, fieldType.getName());
163                }
164                addCode(codeList, errorCode, null, null);
165                return StringUtils.toStringArray(codeList);
166        }
167
168        private void addCodes(Collection<String> codeList, String errorCode, @Nullable String objectName, Iterable<String> fields) {
169                for (String field : fields) {
170                        addCode(codeList, errorCode, objectName, field);
171                }
172        }
173
174        private void addCode(Collection<String> codeList, String errorCode, @Nullable String objectName, @Nullable String field) {
175                codeList.add(postProcessMessageCode(this.formatter.format(errorCode, objectName, field)));
176        }
177
178        /**
179         * Add both keyed and non-keyed entries for the supplied {@code field}
180         * to the supplied field list.
181         */
182        protected void buildFieldList(String field, List<String> fieldList) {
183                fieldList.add(field);
184                String plainField = field;
185                int keyIndex = plainField.lastIndexOf('[');
186                while (keyIndex != -1) {
187                        int endKeyIndex = plainField.indexOf(']', keyIndex);
188                        if (endKeyIndex != -1) {
189                                plainField = plainField.substring(0, keyIndex) + plainField.substring(endKeyIndex + 1);
190                                fieldList.add(plainField);
191                                keyIndex = plainField.lastIndexOf('[');
192                        }
193                        else {
194                                keyIndex = -1;
195                        }
196                }
197        }
198
199        /**
200         * Post-process the given message code, built by this resolver.
201         * <p>The default implementation applies the specified prefix, if any.
202         * @param code the message code as built by this resolver
203         * @return the final message code to be returned
204         * @see #setPrefix
205         */
206        protected String postProcessMessageCode(String code) {
207                return getPrefix() + code;
208        }
209
210
211        /**
212         * Common message code formats.
213         * @see MessageCodeFormatter
214         * @see DefaultMessageCodesResolver#setMessageCodeFormatter(MessageCodeFormatter)
215         */
216        public enum Format implements MessageCodeFormatter {
217
218                /**
219                 * Prefix the error code at the beginning of the generated message code. e.g.:
220                 * {@code errorCode + "." + object name + "." + field}
221                 */
222                PREFIX_ERROR_CODE {
223                        @Override
224                        public String format(String errorCode, @Nullable String objectName, @Nullable String field) {
225                                return toDelimitedString(errorCode, objectName, field);
226                        }
227                },
228
229                /**
230                 * Postfix the error code at the end of the generated message code. e.g.:
231                 * {@code object name + "." + field + "." + errorCode}
232                 */
233                POSTFIX_ERROR_CODE {
234                        @Override
235                        public String format(String errorCode, @Nullable String objectName, @Nullable String field) {
236                                return toDelimitedString(objectName, field, errorCode);
237                        }
238                };
239
240                /**
241                 * Concatenate the given elements, delimiting each with
242                 * {@link DefaultMessageCodesResolver#CODE_SEPARATOR}, skipping zero-length or
243                 * null elements altogether.
244                 */
245                public static String toDelimitedString(String... elements) {
246                        StringJoiner rtn = new StringJoiner(CODE_SEPARATOR);
247                        for (String element : elements) {
248                                if (StringUtils.hasLength(element)) {
249                                        rtn.add(element);
250                                }
251                        }
252                        return rtn.toString();
253                }
254        }
255
256}