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}