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}