001/* 002 * Copyright 2002-2020 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.ArrayDeque; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.Deque; 024import java.util.List; 025import java.util.NoSuchElementException; 026 027import org.springframework.lang.Nullable; 028import org.springframework.util.StringUtils; 029 030/** 031 * Abstract implementation of the {@link Errors} interface. Provides common 032 * access to evaluated errors; however, does not define concrete management 033 * of {@link ObjectError ObjectErrors} and {@link FieldError FieldErrors}. 034 * 035 * @author Juergen Hoeller 036 * @author Rossen Stoyanchev 037 * @since 2.5.3 038 */ 039@SuppressWarnings("serial") 040public abstract class AbstractErrors implements Errors, Serializable { 041 042 private String nestedPath = ""; 043 044 private final Deque<String> nestedPathStack = new ArrayDeque<>(); 045 046 047 @Override 048 public void setNestedPath(@Nullable String nestedPath) { 049 doSetNestedPath(nestedPath); 050 this.nestedPathStack.clear(); 051 } 052 053 @Override 054 public String getNestedPath() { 055 return this.nestedPath; 056 } 057 058 @Override 059 public void pushNestedPath(String subPath) { 060 this.nestedPathStack.push(getNestedPath()); 061 doSetNestedPath(getNestedPath() + subPath); 062 } 063 064 @Override 065 public void popNestedPath() throws IllegalStateException { 066 try { 067 String formerNestedPath = this.nestedPathStack.pop(); 068 doSetNestedPath(formerNestedPath); 069 } 070 catch (NoSuchElementException ex) { 071 throw new IllegalStateException("Cannot pop nested path: no nested path on stack"); 072 } 073 } 074 075 /** 076 * Actually set the nested path. 077 * Delegated to by setNestedPath and pushNestedPath. 078 */ 079 protected void doSetNestedPath(@Nullable String nestedPath) { 080 if (nestedPath == null) { 081 nestedPath = ""; 082 } 083 nestedPath = canonicalFieldName(nestedPath); 084 if (nestedPath.length() > 0 && !nestedPath.endsWith(Errors.NESTED_PATH_SEPARATOR)) { 085 nestedPath += Errors.NESTED_PATH_SEPARATOR; 086 } 087 this.nestedPath = nestedPath; 088 } 089 090 /** 091 * Transform the given field into its full path, 092 * regarding the nested path of this instance. 093 */ 094 protected String fixedField(@Nullable String field) { 095 if (StringUtils.hasLength(field)) { 096 return getNestedPath() + canonicalFieldName(field); 097 } 098 else { 099 String path = getNestedPath(); 100 return (path.endsWith(Errors.NESTED_PATH_SEPARATOR) ? 101 path.substring(0, path.length() - NESTED_PATH_SEPARATOR.length()) : path); 102 } 103 } 104 105 /** 106 * Determine the canonical field name for the given field. 107 * <p>The default implementation simply returns the field name as-is. 108 * @param field the original field name 109 * @return the canonical field name 110 */ 111 protected String canonicalFieldName(String field) { 112 return field; 113 } 114 115 116 @Override 117 public void reject(String errorCode) { 118 reject(errorCode, null, null); 119 } 120 121 @Override 122 public void reject(String errorCode, String defaultMessage) { 123 reject(errorCode, null, defaultMessage); 124 } 125 126 @Override 127 public void rejectValue(@Nullable String field, String errorCode) { 128 rejectValue(field, errorCode, null, null); 129 } 130 131 @Override 132 public void rejectValue(@Nullable String field, String errorCode, String defaultMessage) { 133 rejectValue(field, errorCode, null, defaultMessage); 134 } 135 136 137 @Override 138 public boolean hasErrors() { 139 return !getAllErrors().isEmpty(); 140 } 141 142 @Override 143 public int getErrorCount() { 144 return getAllErrors().size(); 145 } 146 147 @Override 148 public List<ObjectError> getAllErrors() { 149 List<ObjectError> result = new ArrayList<>(); 150 result.addAll(getGlobalErrors()); 151 result.addAll(getFieldErrors()); 152 return Collections.unmodifiableList(result); 153 } 154 155 @Override 156 public boolean hasGlobalErrors() { 157 return (getGlobalErrorCount() > 0); 158 } 159 160 @Override 161 public int getGlobalErrorCount() { 162 return getGlobalErrors().size(); 163 } 164 165 @Override 166 @Nullable 167 public ObjectError getGlobalError() { 168 List<ObjectError> globalErrors = getGlobalErrors(); 169 return (!globalErrors.isEmpty() ? globalErrors.get(0) : null); 170 } 171 172 @Override 173 public boolean hasFieldErrors() { 174 return (getFieldErrorCount() > 0); 175 } 176 177 @Override 178 public int getFieldErrorCount() { 179 return getFieldErrors().size(); 180 } 181 182 @Override 183 @Nullable 184 public FieldError getFieldError() { 185 List<FieldError> fieldErrors = getFieldErrors(); 186 return (!fieldErrors.isEmpty() ? fieldErrors.get(0) : null); 187 } 188 189 @Override 190 public boolean hasFieldErrors(String field) { 191 return (getFieldErrorCount(field) > 0); 192 } 193 194 @Override 195 public int getFieldErrorCount(String field) { 196 return getFieldErrors(field).size(); 197 } 198 199 @Override 200 public List<FieldError> getFieldErrors(String field) { 201 List<FieldError> fieldErrors = getFieldErrors(); 202 List<FieldError> result = new ArrayList<>(); 203 String fixedField = fixedField(field); 204 for (FieldError error : fieldErrors) { 205 if (isMatchingFieldError(fixedField, error)) { 206 result.add(error); 207 } 208 } 209 return Collections.unmodifiableList(result); 210 } 211 212 @Override 213 @Nullable 214 public FieldError getFieldError(String field) { 215 List<FieldError> fieldErrors = getFieldErrors(field); 216 return (!fieldErrors.isEmpty() ? fieldErrors.get(0) : null); 217 } 218 219 @Override 220 @Nullable 221 public Class<?> getFieldType(String field) { 222 Object value = getFieldValue(field); 223 return (value != null ? value.getClass() : null); 224 } 225 226 /** 227 * Check whether the given FieldError matches the given field. 228 * @param field the field that we are looking up FieldErrors for 229 * @param fieldError the candidate FieldError 230 * @return whether the FieldError matches the given field 231 */ 232 protected boolean isMatchingFieldError(String field, FieldError fieldError) { 233 if (field.equals(fieldError.getField())) { 234 return true; 235 } 236 // Optimization: use charAt and regionMatches instead of endsWith and startsWith (SPR-11304) 237 int endIndex = field.length() - 1; 238 return (endIndex >= 0 && field.charAt(endIndex) == '*' && 239 (endIndex == 0 || field.regionMatches(0, fieldError.getField(), 0, endIndex))); 240 } 241 242 243 @Override 244 public String toString() { 245 StringBuilder sb = new StringBuilder(getClass().getName()); 246 sb.append(": ").append(getErrorCount()).append(" errors"); 247 for (ObjectError error : getAllErrors()) { 248 sb.append('\n').append(error); 249 } 250 return sb.toString(); 251 } 252 253}