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}