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.io.Serializable;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.List;
026import java.util.Locale;
027import java.util.MissingResourceException;
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 #MESSAGES_KEY} key in {@value #MESSAGES_BUNDLE} bundle to customize default message</li>
043 * <li>Append the constraint name to the previous key to customize the generic message to some constraint</li>
044 * <li>Append the schema and the field name to the previous key to customize the message for a specific constraint
045 * applied to some specific schema field.</li>
046 * </ul>
047 * <br>
048 * For each messages, you can use parameters in the message :
049 * <ul>
050 * <li>The invalid value : {0}</li>
051 * <li>The schema name : {1}</li>
052 * <li>The field name : {2}</li>
053 * <li>The constraint name : {3}</li>
054 * <li>The first constraint parameter (if exists) : {4}</li>
055 * <li>The second constraint parameter (if exists) : {5}</li>
056 * <li>...</li>
057 * </ul>
058 * </p>
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 * </p>
069 *
070 * @since 7.1
071 */
072public class ConstraintViolation implements Serializable {
073
074    private static final Log log = LogFactory.getLog(ConstraintViolation.class);
075
076    private static final long serialVersionUID = 1L;
077
078    private final Schema schema;
079
080    private final List<PathNode> path;
081
082    private final Constraint constraint;
083
084    private final Object invalidValue;
085
086    public ConstraintViolation(Schema schema, List<PathNode> fieldPath, Constraint constraint, Object invalidValue) {
087        this.schema = schema;
088        path = new ArrayList<>(fieldPath);
089        this.constraint = constraint;
090        this.invalidValue = invalidValue;
091    }
092
093    public Schema getSchema() {
094        return schema;
095    }
096
097    public List<PathNode> getPath() {
098        return Collections.unmodifiableList(path);
099    }
100
101    public Constraint getConstraint() {
102        return constraint;
103    }
104
105    public Object getInvalidValue() {
106        return invalidValue;
107    }
108
109    /**
110     * @return The message if it's found in message bundles, a generic message otherwise.
111     * @since 7.1
112     */
113    public String getMessage(Locale locale) {
114        // test whether there's a specific translation for for this field and this constraint
115        // the expected key is label.schema.constraint.violation.[constraintName].[schemaName].[field].[subField]
116        List<String> pathTokens = new ArrayList<>();
117        pathTokens.add(Constraint.MESSAGES_KEY);
118        pathTokens.add(constraint.getDescription().getName());
119        pathTokens.add(schema.getName());
120        for (PathNode node : path) {
121            String name = node.getField().getName().getLocalName();
122            pathTokens.add(name);
123        }
124        String key = StringUtils.join(pathTokens, '.');
125        String computedInvalidValue = "null";
126        if (invalidValue != null) {
127            String invalidValueString = invalidValue.toString();
128            if (invalidValueString.length() > 20) {
129                computedInvalidValue = invalidValueString.substring(0, 15) + "...";
130            } else {
131                computedInvalidValue = invalidValueString;
132            }
133        }
134        Object[] params = new Object[] { computedInvalidValue };
135        Locale computedLocale = locale != null ? locale : Constraint.MESSAGES_DEFAULT_LANG;
136        String message;
137        try {
138            message = I18NUtils.getMessageString(Constraint.MESSAGES_BUNDLE, key, params, computedLocale);
139        } catch (MissingResourceException e) {
140            log.trace("No bundle found", e);
141            message = null;
142        }
143        if (message != null && !message.trim().isEmpty() && !key.equals(message)) {
144            // use the message if there's one
145            return message;
146        } else {
147            if (locale != null && Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
148                // use the constraint message
149                return constraint.getErrorMessage(invalidValue, locale);
150            } else {
151                return getMessage(Locale.ENGLISH);
152            }
153        }
154    }
155
156    @Override
157    public String toString() {
158        return getMessage(Locale.ENGLISH);
159    }
160
161    /**
162     * Allows to locates some constraint violation in a document.
163     * <p>
164     * {@link #getIndex()} are used to indicates which element violates the constraint for list properties.
165     * </p>
166     *
167     * @since 7.1
168     */
169    public static class PathNode {
170
171        private Field field;
172
173        private boolean listItem = false;
174
175        int index = 0;
176
177        public PathNode(Field field) {
178            this.field = field;
179        }
180
181        public PathNode(Field field, int index) {
182            super();
183            this.field = field;
184            this.index = index;
185            listItem = true;
186        }
187
188        public Field getField() {
189            return field;
190        }
191
192        public int getIndex() {
193            return index;
194        }
195
196        public boolean isListItem() {
197            return listItem;
198        }
199
200        @Override
201        public String toString() {
202            if (listItem) {
203                return field.getName().getPrefixedName();
204            } else {
205                return field.getName().getPrefixedName() + "[" + index + "]";
206            }
207        }
208
209    }
210
211}