001/*
002 * (C) Copyright 2014-2018 Nuxeo (http://nuxeo.com/) and others.
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 *     http://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 * Contributors:
017 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
018 */
019
020package org.nuxeo.ecm.core.api.validation;
021
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Locale;
026import java.util.MissingResourceException;
027import java.util.stream.Collectors;
028
029import org.apache.commons.lang3.StringUtils;
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.common.utils.i18n.I18NUtils;
033import org.nuxeo.ecm.core.schema.types.Field;
034import org.nuxeo.ecm.core.schema.types.Schema;
035import org.nuxeo.ecm.core.schema.types.constraints.Constraint;
036
037/**
038 * A constraint violation description. Use {@link #getMessage(Locale)} to get the constraint violation description.
039 * <p>
040 * You could customize constraint violation message using the following rules :
041 * <ul>
042 * <li>Use {@value Constraint#MESSAGES_KEY} key in {@value Constraint#MESSAGES_BUNDLE} bundle to customize default
043 * message</li>
044 * <li>Append the constraint name to the previous key to customize the generic message to some constraint</li>
045 * <li>Append the schema and the field name to the previous key to customize the message for a specific constraint
046 * applied to some specific schema field.</li>
047 * </ul>
048 * <br>
049 * For each messages, you can use parameters in the message :
050 * <ul>
051 * <li>The invalid value : {0}</li>
052 * <li>The schema name : {1}</li>
053 * <li>The field name : {2}</li>
054 * <li>The constraint name : {3}</li>
055 * <li>The first constraint parameter (if exists) : {4}</li>
056 * <li>The second constraint parameter (if exists) : {5}</li>
057 * <li>...</li>
058 * </ul>
059 * <p>
060 * Examples :
061 * <ul>
062 * <li>label.schema.constraint.violation=Value '{0}' for field '{1}.{2}' does not respect constraint '{3}'</li>
063 * <li>label.schema.constraint.violation.PatternConstraint='{1}.{2}' value ({0}) should match the following format :
064 * '{4}'</li>
065 * <li>label.schema.constraint.violation.PatternConstraint.myuserschema.firstname ='The firstname should not be empty'
066 * </li>
067 * </ul>
068 *
069 * @since 7.1
070 */
071public class ConstraintViolation implements ValidationViolation {
072
073    private static final Log log = LogFactory.getLog(ConstraintViolation.class);
074
075    private static final long serialVersionUID = 1L;
076
077    private final Schema schema;
078
079    private final List<PathNode> path;
080
081    private final Constraint constraint;
082
083    private final Object invalidValue;
084
085    public ConstraintViolation(Schema schema, List<PathNode> fieldPath, Constraint constraint, Object invalidValue) {
086        this.schema = schema;
087        path = new ArrayList<>(fieldPath);
088        this.constraint = constraint;
089        this.invalidValue = invalidValue;
090    }
091
092    public Schema getSchema() {
093        return schema;
094    }
095
096    public List<PathNode> getPath() {
097        return Collections.unmodifiableList(path);
098    }
099
100    public String getPathAsString() {
101        return path.stream().map(PathNode::toString).collect(Collectors.joining("/"));
102    }
103
104    public Constraint getConstraint() {
105        return constraint;
106    }
107
108    public Object getInvalidValue() {
109        return invalidValue;
110    }
111
112    /**
113     * @return The message if it's found in message bundles, a generic message otherwise.
114     * @since 7.1
115     */
116    @Override
117    public String getMessage(Locale locale) {
118        String key = getMessageKey();
119        String computedInvalidValue = "null";
120        if (invalidValue != null) {
121            String invalidValueString = invalidValue.toString();
122            if (invalidValueString.length() > 20) {
123                computedInvalidValue = invalidValueString.substring(0, 15) + "...";
124            } else {
125                computedInvalidValue = invalidValueString;
126            }
127        }
128        Object[] params = new Object[] { computedInvalidValue };
129        Locale computedLocale = locale != null ? locale : Constraint.MESSAGES_DEFAULT_LANG;
130        String message;
131        try {
132            message = I18NUtils.getMessageString(Constraint.MESSAGES_BUNDLE, key, params, computedLocale);
133        } catch (MissingResourceException e) {
134            log.trace("No bundle found", e);
135            message = null;
136        }
137        if (message != null && !message.trim().isEmpty() && !key.equals(message)) {
138            // use the message if there's one
139            return message;
140        } else {
141            if (locale != null && Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
142                // use the constraint message
143                return constraint.getErrorMessage(invalidValue, locale);
144            } else {
145                return getMessage(Locale.ENGLISH);
146            }
147        }
148    }
149
150    @Override
151    public String toString() {
152        return getMessage(Locale.ENGLISH);
153    }
154
155    /**
156     * Allows to locates some constraint violation in a document.
157     * <p>
158     * {@link #getIndex()} are used to indicates which element violates the constraint for list properties.
159     * </p>
160     *
161     * @since 7.1
162     */
163    public static class PathNode {
164
165        private Field field;
166
167        private boolean listItem = false;
168
169        int index = 0;
170
171        public PathNode(Field field) {
172            this.field = field;
173        }
174
175        public PathNode(Field field, int index) {
176            super();
177            this.field = field;
178            this.index = index;
179            listItem = true;
180        }
181
182        public Field getField() {
183            return field;
184        }
185
186        public int getIndex() {
187            return index;
188        }
189
190        public boolean isListItem() {
191            return listItem;
192        }
193
194        @Override
195        public String toString() {
196            if (listItem) {
197                return field.getName().getPrefixedName();
198            } else {
199                return field.getName().getPrefixedName() + "[" + index + "]";
200            }
201        }
202
203    }
204
205    @Override
206    public String getMessageKey() {
207        // test whether there's a specific translation for this field and this constraint
208        // the expected key is label.schema.constraint.violation.[constraintName].[schemaName].[field].[subField]
209        List<String> pathTokens = new ArrayList<>();
210        pathTokens.add(constraint.getMessageKey());
211        pathTokens.add(schema.getName());
212        for (PathNode node : path) {
213            String name = node.getField().getName().getLocalName();
214            pathTokens.add(name);
215        }
216        return StringUtils.join(pathTokens, '.');
217    }
218
219}