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